forked from IQ.Lvbs/IQ.Pilot
IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
548
iqpilot/tools/cabana/ui/widgets/binaryview.cc
Normal file
548
iqpilot/tools/cabana/ui/widgets/binaryview.cc
Normal file
@@ -0,0 +1,548 @@
|
||||
#include "tools/cabana/ui/widgets/binaryview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const int CELL_HEIGHT = 36;
|
||||
const float SMALL_FONT_SIZE = 10.0f;
|
||||
const int VERTICAL_HEADER_WIDTH = 30;
|
||||
inline int get_bit_pos(const BinaryIndex &index) { return flipBitPos(index.row * 8 + index.column); }
|
||||
|
||||
inline ImU32 paletteHighlight() { return ImGui::GetColorU32(ImGuiCol_Header); }
|
||||
inline ImU32 paletteBase() { return ImGui::GetColorU32(ImGuiCol_ChildBg); }
|
||||
inline ImU32 paletteText(bool active) { return ImGui::GetColorU32(active ? ImGuiCol_Text : ImGuiCol_TextDisabled); }
|
||||
const ImU32 DARK_GRAY = IM_COL32(128, 128, 128, 255);
|
||||
|
||||
|
||||
|
||||
void drawBoldText(ImDrawList *p, const ImRect &r, const char *text, ImU32 col, ImFont *font, float font_size) {
|
||||
drawText(p, r, text, col, font, font_size);
|
||||
drawText(p, ImRect(ImVec2(r.Min.x + 0.6f, r.Min.y), ImVec2(r.Max.x + 0.6f, r.Max.y)), text, col, font, font_size);
|
||||
}
|
||||
|
||||
|
||||
void fillDense7Pattern(ImDrawList *p, const ImRect &r, ImU32 col) {
|
||||
p->PushClipRect(r.Min, r.Max, true);
|
||||
for (float y = r.Min.y; y < r.Max.y; y += 4.0f) {
|
||||
for (float x = r.Min.x + (static_cast<int>((y - r.Min.y) / 4.0f) % 2) * 2.0f; x < r.Max.x; x += 4.0f) {
|
||||
p->AddRectFilled(ImVec2(x, y), ImVec2(x + 1.0f, y + 1.0f), col);
|
||||
}
|
||||
}
|
||||
p->PopClipRect();
|
||||
}
|
||||
|
||||
|
||||
void fillBDiagPattern(ImDrawList *p, const ImRect &r, ImU32 col) {
|
||||
p->PushClipRect(r.Min, r.Max, true);
|
||||
const float h = r.GetHeight();
|
||||
for (float x = r.Min.x - h; x < r.Max.x; x += 8.0f) {
|
||||
p->AddLine(ImVec2(x, r.Max.y), ImVec2(x + h, r.Min.y), col, 1.0f);
|
||||
}
|
||||
p->PopClipRect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BinaryView::BinaryView() {
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
}
|
||||
|
||||
std::string BinaryView::whatsThis() const {
|
||||
return R"(
|
||||
<b>Binary View</b><br/>
|
||||
<span style="color:gray">Shortcuts</span><br />
|
||||
Delete Signal:
|
||||
<span style="background-color:lightGray;color:gray"> x </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Backspace </span>,
|
||||
<span style="background-color:lightGray;color:gray"> Delete </span><br />
|
||||
Change endianness: <span style="background-color:lightGray;color:gray"> e </span><br />
|
||||
Change signedness: <span style="background-color:lightGray;color:gray"> s </span><br />
|
||||
Open chart:
|
||||
<span style="background-color:lightGray;color:gray"> c </span>,
|
||||
<span style="background-color:lightGray;color:gray"> p </span>,
|
||||
<span style="background-color:lightGray;color:gray"> g </span>
|
||||
)";
|
||||
}
|
||||
|
||||
void BinaryView::addShortcuts() {
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
if (io.WantTextInput || io.KeyCtrl || io.KeySuper) return;
|
||||
if (ImGui::GetTopMostPopupModal() != nullptr) return;
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_X, false) || ImGui::IsKeyPressed(ImGuiKey_Backspace, false) || ImGui::IsKeyPressed(ImGuiKey_Delete, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
UndoStack::instance()->push(new RemoveSigCommand(msg_id_, hovered_sig_));
|
||||
hovered_sig_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_E, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
cabana::Signal s = *hovered_sig_;
|
||||
s.is_little_endian = !s.is_little_endian;
|
||||
editSignal(hovered_sig_, s);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_S, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
cabana::Signal s = *hovered_sig_;
|
||||
s.is_signed = !s.is_signed;
|
||||
editSignal(hovered_sig_, s);
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_P, false) || ImGui::IsKeyPressed(ImGuiKey_G, false) || ImGui::IsKeyPressed(ImGuiKey_C, false)) {
|
||||
if (hovered_sig_ != nullptr) {
|
||||
showChart(msg_id_, hovered_sig_, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImVec2 BinaryView::minimumSizeHint() const {
|
||||
|
||||
pushMonoFont();
|
||||
const float min_section_size = ImGui::CalcTextSize("W").x + 8.0f;
|
||||
popMonoFont();
|
||||
return {(min_section_size + 1) * 9 + VERTICAL_HEADER_WIDTH + 2,
|
||||
static_cast<float>(CELL_HEIGHT * std::min(row_count_, 10) + 2)};
|
||||
}
|
||||
|
||||
void BinaryView::highlight(const cabana::Signal *sig) {
|
||||
if (sig != hovered_sig_) {
|
||||
hovered_sig_ = sig;
|
||||
signalHovered(hovered_sig_);
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::setSelection() {
|
||||
auto index = indexAt(last_mouse_pos_);
|
||||
if (!anchor_index_.isValid() || !index.isValid())
|
||||
return;
|
||||
|
||||
std::set<BinaryIndex> selection;
|
||||
auto [start, size, is_lb] = getSelection(index);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
int pos = is_lb ? flipBitPos(start + i) : flipBitPos(start) + i;
|
||||
selection.insert({pos / 8, pos % 8});
|
||||
}
|
||||
selection_ = std::move(selection);
|
||||
}
|
||||
|
||||
void BinaryView::handleMousePress(const ImVec2 &pos) {
|
||||
resize_sig_ = nullptr;
|
||||
if (auto index = indexAt(last_mouse_pos_ = pos); index.isValid() && index.column != HEX_COLUMN) {
|
||||
anchor_index_ = index;
|
||||
auto item = &cellAt(anchor_index_);
|
||||
int bit_pos = get_bit_pos(anchor_index_);
|
||||
for (auto s : item->sigs) {
|
||||
if (bit_pos == s->lsb || bit_pos == s->msb) {
|
||||
int idx = flipBitPos(bit_pos == s->lsb ? s->msb : s->lsb);
|
||||
anchor_index_ = {idx / 8, idx % 8};
|
||||
resize_sig_ = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::highlightPosition(const ImVec2 &pos) {
|
||||
if (auto index = indexAt(pos); index.isValid()) {
|
||||
auto item = &cellAt(index);
|
||||
const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
|
||||
highlight(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void BinaryView::handleMouseMove(const ImVec2 &pos) {
|
||||
highlightPosition(last_mouse_pos_ = pos);
|
||||
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Left) && indexAt(pos).column != HEX_COLUMN) setSelection();
|
||||
}
|
||||
|
||||
void BinaryView::handleMouseRelease(const ImVec2 &pos) {
|
||||
auto release_index = indexAt(pos);
|
||||
if (release_index.isValid() && anchor_index_.isValid()) {
|
||||
if (hasSelection()) {
|
||||
auto sig = resize_sig_ ? *resize_sig_ : cabana::Signal{};
|
||||
std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index);
|
||||
resize_sig_ ? editSignal(resize_sig_, sig)
|
||||
: UndoStack::instance()->push(new AddSigCommand(msg_id_, sig));
|
||||
} else {
|
||||
auto item = &cellAt(anchor_index_);
|
||||
if (item->sigs.size() > 0)
|
||||
signalClicked(item->sigs.back());
|
||||
}
|
||||
}
|
||||
selection_.clear();
|
||||
anchor_index_ = BinaryIndex();
|
||||
resize_sig_ = nullptr;
|
||||
}
|
||||
|
||||
void BinaryView::setMessage(const MessageId &message_id) {
|
||||
msg_id_ = message_id;
|
||||
scroll_to_top_ = true;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void BinaryView::refresh() {
|
||||
selection_.clear();
|
||||
anchor_index_ = BinaryIndex();
|
||||
resize_sig_ = nullptr;
|
||||
hovered_sig_ = nullptr;
|
||||
bit_flip_tracker_ = {};
|
||||
cells_.clear();
|
||||
if (auto dbc_msg = dbc()->msg(msg_id_)) {
|
||||
row_count_ = dbc_msg->size;
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
for (auto sig : dbc_msg->getSignals()) {
|
||||
for (int j = 0; j < sig->size; ++j) {
|
||||
int pos = sig->is_little_endian ? flipBitPos(sig->start_bit + j) : flipBitPos(sig->start_bit) + j;
|
||||
int idx = COLUMN_COUNT * (pos / 8) + pos % 8;
|
||||
if (idx >= cells_.size()) {
|
||||
fprintf(stderr, "signal %s out of bounds.start_bit: %d size: %d\n", sig->name.c_str(), sig->start_bit, sig->size);
|
||||
break;
|
||||
}
|
||||
if (j == 0) sig->is_little_endian ? cells_[idx].is_lsb = true : cells_[idx].is_msb = true;
|
||||
if (j == sig->size - 1) sig->is_little_endian ? cells_[idx].is_msb = true : cells_[idx].is_lsb = true;
|
||||
|
||||
auto &sigs = cells_[idx].sigs;
|
||||
sigs.push_back(sig);
|
||||
if (sigs.size() > 1) {
|
||||
std::sort(sigs.begin(), sigs.end(), [](auto l, auto r) { return l->size > r->size; });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row_count_ = can->lastMessage(msg_id_).dat.size();
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
}
|
||||
updateState();
|
||||
if (under_mouse_) highlightPosition(last_mouse_pos_);
|
||||
}
|
||||
|
||||
|
||||
std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
|
||||
std::set<const cabana::Signal *> overlapping;
|
||||
for (const auto &item : cells_) {
|
||||
if (item.sigs.size() > 1) {
|
||||
for (auto s : item.sigs) {
|
||||
if (s->type == cabana::Signal::Type::Normal) overlapping.insert(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
return overlapping;
|
||||
}
|
||||
|
||||
std::tuple<int, int, bool> BinaryView::getSelection(BinaryIndex index) {
|
||||
if (index.column == HEX_COLUMN) {
|
||||
index = {index.row, 7};
|
||||
}
|
||||
bool is_lb = true;
|
||||
if (resize_sig_) {
|
||||
is_lb = resize_sig_->is_little_endian;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::MsbFirst) {
|
||||
is_lb = index < anchor_index_;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::LsbFirst) {
|
||||
is_lb = !(index < anchor_index_);
|
||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysLE) {
|
||||
is_lb = true;
|
||||
} else if (settings.drag_direction == Settings::DragDirection::AlwaysBE) {
|
||||
is_lb = false;
|
||||
}
|
||||
|
||||
int cur_bit_pos = get_bit_pos(index);
|
||||
int anchor_bit_pos = get_bit_pos(anchor_index_);
|
||||
int start_bit = is_lb ? std::min(cur_bit_pos, anchor_bit_pos) : get_bit_pos(std::min(index, anchor_index_));
|
||||
int size = is_lb ? std::abs(cur_bit_pos - anchor_bit_pos) + 1 : std::abs(flipBitPos(cur_bit_pos) - flipBitPos(anchor_bit_pos)) + 1;
|
||||
return {start_bit, size, is_lb};
|
||||
}
|
||||
|
||||
BinaryIndex BinaryView::indexAt(const ImVec2 &pos) const {
|
||||
if (column_width_ <= 0 || pos.x < grid_pos_.x + VERTICAL_HEADER_WIDTH || pos.y < grid_pos_.y) return {};
|
||||
int column = static_cast<int>((pos.x - grid_pos_.x - VERTICAL_HEADER_WIDTH) / column_width_);
|
||||
int row = static_cast<int>((pos.y - grid_pos_.y) / CELL_HEIGHT);
|
||||
if (column >= COLUMN_COUNT || row >= row_count_) return {};
|
||||
return {row, column};
|
||||
}
|
||||
|
||||
ImRect BinaryView::visualRect(const BinaryIndex &index) const {
|
||||
|
||||
|
||||
const float x0 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND(index.column * column_width_);
|
||||
const float x1 = grid_pos_.x + VERTICAL_HEADER_WIDTH + IM_ROUND((index.column + 1) * column_width_);
|
||||
const float y = grid_pos_.y + index.row * CELL_HEIGHT;
|
||||
return ImRect(x0, y, x1, y + CELL_HEIGHT);
|
||||
}
|
||||
|
||||
void BinaryView::draw() {
|
||||
is_message_active_ = can->isMessageActive(msg_id_);
|
||||
if (scroll_to_top_) {
|
||||
ImGui::SetScrollY(0.0f);
|
||||
scroll_to_top_ = false;
|
||||
}
|
||||
|
||||
const int rows = row_count_;
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
column_width_ = std::max(1.0f, (width - VERTICAL_HEADER_WIDTH) / COLUMN_COUNT);
|
||||
grid_pos_ = ImGui::GetCursorScreenPos();
|
||||
ImGui::InvisibleButton("##binary_view", ImVec2(std::max(width, 1.0f), std::max(static_cast<float>(rows * CELL_HEIGHT), 1.0f)));
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
const ImRect r(grid_pos_.x, grid_pos_.y + row * CELL_HEIGHT, grid_pos_.x + VERTICAL_HEADER_WIDTH, grid_pos_.y + (row + 1) * CELL_HEIGHT);
|
||||
painter->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
drawText(painter, r, std::to_string(row).c_str(), ImGui::GetColorU32(ImGuiCol_Text));
|
||||
}
|
||||
for (int row = 0; row < rows; ++row) {
|
||||
for (int column = 0; column < COLUMN_COUNT; ++column) {
|
||||
const BinaryIndex index = {row, column};
|
||||
paintCell(painter, visualRect(index), index);
|
||||
}
|
||||
}
|
||||
|
||||
const ImVec2 mouse = ImGui::GetMousePos();
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
const bool active = ImGui::IsItemActive();
|
||||
const bool under_mouse = (hovered || active) && ImGui::IsMouseHoveringRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), false);
|
||||
if (hovered || active) {
|
||||
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) handleMousePress(mouse);
|
||||
const ImVec2 delta = ImGui::GetIO().MouseDelta;
|
||||
if (delta.x != 0.0f || delta.y != 0.0f) {
|
||||
handleMouseMove(mouse);
|
||||
} else {
|
||||
|
||||
|
||||
highlightPosition(last_mouse_pos_ = mouse);
|
||||
}
|
||||
}
|
||||
|
||||
if (std::exchange(under_mouse_, under_mouse) && !under_mouse) highlight(nullptr);
|
||||
if (ImGui::IsItemDeactivated()) handleMouseRelease(mouse);
|
||||
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) {
|
||||
if (auto index = indexAt(mouse); index.isValid() && !cellAt(index).sigs.empty()) {
|
||||
ImGui::SetTooltip("%s", utils::stripHtml(utils::signalToolTip(cellAt(index).sigs.back())).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
addShortcuts();
|
||||
}
|
||||
|
||||
void BinaryView::setCell(int row, int col, uint8_t val, const CabanaColor &color) {
|
||||
auto &item = cells_[row * COLUMN_COUNT + col];
|
||||
item.valid = true;
|
||||
item.val = val;
|
||||
item.bg_color = color;
|
||||
}
|
||||
|
||||
void BinaryView::updateState() {
|
||||
const auto &last_msg = can->lastMessage(msg_id_);
|
||||
const auto &binary = last_msg.dat;
|
||||
if (binary.size() > row_count_) {
|
||||
row_count_ = binary.size();
|
||||
cells_.resize(row_count_ * COLUMN_COUNT);
|
||||
}
|
||||
|
||||
auto &bit_flips = heatmap_live_mode_ ? last_msg.bit_flip_counts : bitFlipChanges(binary.size());
|
||||
uint32_t max_bit_flip_count = 1;
|
||||
for (const auto &row : bit_flips) {
|
||||
for (uint32_t count : row) {
|
||||
max_bit_flip_count = std::max(max_bit_flip_count, count);
|
||||
}
|
||||
}
|
||||
|
||||
const bool dark = isDarkTheme();
|
||||
const double max_alpha = 255.0;
|
||||
const double min_alpha_with_signal = dark ? 70.0 : 25.0;
|
||||
const double min_alpha_no_signal = dark ? 28.0 : 10.0;
|
||||
const double alpha_gamma = dark ? 0.6 : 1.0;
|
||||
const double log_factor = 1.0 + 0.2;
|
||||
const double log_scaler = max_alpha / log2(log_factor * max_bit_flip_count);
|
||||
|
||||
for (size_t i = 0; i < binary.size(); ++i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
auto &item = cells_[i * COLUMN_COUNT + j];
|
||||
int bit_val = (binary[i] >> (7 - j)) & 1;
|
||||
|
||||
double alpha = item.sigs.empty() ? 0 : min_alpha_with_signal;
|
||||
uint32_t flip_count = bit_flips[i][j];
|
||||
if (flip_count > 0) {
|
||||
double normalized_alpha = log2(1.0 + flip_count * log_factor) * log_scaler;
|
||||
normalized_alpha = max_alpha * std::pow(std::clamp(normalized_alpha / max_alpha, 0.0, 1.0), alpha_gamma);
|
||||
double min_alpha = item.sigs.empty() ? min_alpha_no_signal : min_alpha_with_signal;
|
||||
alpha = std::clamp(normalized_alpha, min_alpha, max_alpha);
|
||||
}
|
||||
|
||||
auto color = item.bg_color;
|
||||
color.a = static_cast<uint8_t>(alpha);
|
||||
setCell(i, j, bit_val, color);
|
||||
}
|
||||
setCell(i, HEX_COLUMN, binary[i], last_msg.colors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::array<uint32_t, 8>> &BinaryView::bitFlipChanges(size_t msg_size) {
|
||||
auto time_range = can->timeRange();
|
||||
if (bit_flip_tracker_.time_range == time_range && !bit_flip_tracker_.flip_counts.empty())
|
||||
return bit_flip_tracker_.flip_counts;
|
||||
|
||||
bit_flip_tracker_.time_range = time_range;
|
||||
bit_flip_tracker_.flip_counts.assign(msg_size, std::array<uint32_t, 8>{});
|
||||
|
||||
auto [first, last] = can->eventsInRange(msg_id_, time_range);
|
||||
if (std::distance(first, last) <= 1) return bit_flip_tracker_.flip_counts;
|
||||
|
||||
std::vector<uint8_t> prev_values((*first)->dat, (*first)->dat + (*first)->size);
|
||||
for (auto it = std::next(first); it != last; ++it) {
|
||||
const CanEvent *event = *it;
|
||||
int size = std::min<int>(msg_size, event->size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
const uint8_t diff = event->dat[i] ^ prev_values[i];
|
||||
if (!diff) continue;
|
||||
|
||||
auto &bit_flips = bit_flip_tracker_.flip_counts[i];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
if (diff & (1u << bit)) ++bit_flips[7 - bit];
|
||||
}
|
||||
prev_values[i] = event->dat[i];
|
||||
}
|
||||
}
|
||||
|
||||
return bit_flip_tracker_.flip_counts;
|
||||
}
|
||||
|
||||
bool BinaryView::hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const {
|
||||
if (!index.isValid()) return false;
|
||||
int idx = (index.row + dy) * COLUMN_COUNT + index.column + dx;
|
||||
if (idx < 0 || idx >= (int)cells_.size()) return false;
|
||||
auto &s = cells_[idx].sigs;
|
||||
return std::find(s.begin(), s.end(), sig) != s.end();
|
||||
}
|
||||
|
||||
void BinaryView::paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const {
|
||||
auto item = &cellAt(index);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
float font_size = ImGui::GetFontSize();
|
||||
ImU32 pen = paletteText(is_message_active_);
|
||||
|
||||
if (index.column == HEX_COLUMN) {
|
||||
if (item->valid) {
|
||||
pushMonoFont();
|
||||
font = ImGui::GetFont();
|
||||
font_size = ImGui::GetFontSize();
|
||||
popMonoFont();
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color));
|
||||
}
|
||||
} else if (isSelected(index)) {
|
||||
auto color = resize_sig_ ? toImU32(resize_sig_->color) : paletteHighlight();
|
||||
painter->AddRectFilled(rect.Min, rect.Max, color);
|
||||
pen = paletteBrightText();
|
||||
} else if (!hasSelection() || std::find(item->sigs.begin(), item->sigs.end(), resize_sig_) == item->sigs.end()) {
|
||||
if (item->sigs.size() > 0) {
|
||||
for (auto &s : item->sigs) {
|
||||
if (s == hovered_sig_) {
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(s->color.darker(125)));
|
||||
} else {
|
||||
drawSignalCell(painter, rect, index, s);
|
||||
}
|
||||
}
|
||||
} else if (item->valid && item->bg_color.alpha() > 0) {
|
||||
painter->AddRectFilled(rect.Min, rect.Max, toImU32(item->bg_color));
|
||||
}
|
||||
bool bright = std::find(item->sigs.begin(), item->sigs.end(), hovered_sig_) != item->sigs.end();
|
||||
pen = bright ? paletteBrightText() : paletteText(is_message_active_);
|
||||
}
|
||||
|
||||
if (item->sigs.size() > 1) {
|
||||
fillDense7Pattern(painter, rect, DARK_GRAY);
|
||||
} else if (!item->valid) {
|
||||
fillBDiagPattern(painter, rect, DARK_GRAY);
|
||||
}
|
||||
if (item->valid) {
|
||||
if (index.column == HEX_COLUMN) {
|
||||
drawBoldText(painter, rect, utils::hexByte(item->val), pen, font, font_size);
|
||||
} else {
|
||||
drawText(painter, rect, item->val ? "1" : "0", pen, font, font_size);
|
||||
}
|
||||
}
|
||||
if (item->is_msb || item->is_lsb) {
|
||||
const ImRect marker_rect(rect.Min, ImVec2(rect.Max.x - 8, rect.Max.y - 3));
|
||||
drawText(painter, marker_rect, item->is_msb ? "M" : "L", pen, nullptr, SMALL_FONT_SIZE, ImVec2(1.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void BinaryView::drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const {
|
||||
bool draw_left = !hasSignal(index, -1, 0, sig);
|
||||
bool draw_top = !hasSignal(index, 0, -1, sig);
|
||||
bool draw_right = !hasSignal(index, 1, 0, sig);
|
||||
bool draw_bottom = !hasSignal(index, 0, 1, sig);
|
||||
|
||||
const int spacing = 2;
|
||||
ImRect rc(rect.Min.x + draw_left * 3, rect.Min.y + draw_top * spacing, rect.Max.x - draw_right * 3, rect.Max.y - draw_bottom * spacing);
|
||||
std::vector<ImRect> subtract;
|
||||
if (!draw_top) {
|
||||
if (!draw_left && !hasSignal(index, -1, -1, sig)) {
|
||||
subtract.emplace_back(rc.Min.x, rc.Min.y, rc.Min.x + 3, rc.Min.y + spacing);
|
||||
} else if (!draw_right && !hasSignal(index, 1, -1, sig)) {
|
||||
subtract.emplace_back(rc.Max.x - 3, rc.Min.y, rc.Max.x, rc.Min.y + spacing);
|
||||
}
|
||||
}
|
||||
if (!draw_bottom) {
|
||||
if (!draw_left && !hasSignal(index, -1, 1, sig)) {
|
||||
subtract.emplace_back(rc.Min.x, rc.Max.y - spacing, rc.Min.x + 3, rc.Max.y);
|
||||
} else if (!draw_right && !hasSignal(index, 1, 1, sig)) {
|
||||
subtract.emplace_back(rc.Max.x - 3, rc.Max.y - spacing, rc.Max.x, rc.Max.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ImRect *top_notch = !subtract.empty() && subtract.front().Min.y == rc.Min.y ? &subtract.front() : nullptr;
|
||||
const ImRect *bottom_notch = !subtract.empty() && subtract.back().Min.y != rc.Min.y ? &subtract.back() : nullptr;
|
||||
std::vector<ImRect> region;
|
||||
auto band = [&](const ImRect *notch, float y0, float y1) {
|
||||
const float x0 = notch && notch->Min.x == rc.Min.x ? notch->Max.x : rc.Min.x;
|
||||
const float x1 = notch && notch->Min.x != rc.Min.x ? notch->Min.x : rc.Max.x;
|
||||
if (x1 > x0 && y1 > y0) region.emplace_back(x0, y0, x1, y1);
|
||||
};
|
||||
if (top_notch) band(top_notch, rc.Min.y, rc.Min.y + spacing);
|
||||
band(nullptr, rc.Min.y + (top_notch ? spacing : 0), rc.Max.y - (bottom_notch ? spacing : 0));
|
||||
if (bottom_notch) band(bottom_notch, rc.Max.y - spacing, rc.Max.y);
|
||||
|
||||
auto item = &cellAt(index);
|
||||
CabanaColor color = sig->color;
|
||||
color.a = item->bg_color.alpha();
|
||||
const ImU32 edge = toImU32(sig->color.darker(125));
|
||||
|
||||
for (const ImRect &clip : region) {
|
||||
painter->PushClipRect(clip.Min, clip.Max, true);
|
||||
|
||||
painter->AddRectFilled(rc.Min, rc.Max, paletteBase());
|
||||
painter->AddRectFilled(rc.Min, rc.Max, toImU32(color));
|
||||
|
||||
if (draw_left) painter->AddLine(ImVec2(rc.Min.x + 0.5f, rc.Min.y), ImVec2(rc.Min.x + 0.5f, rc.Max.y), edge, 1.0f);
|
||||
if (draw_right) painter->AddLine(ImVec2(rc.Max.x - 0.5f, rc.Min.y), ImVec2(rc.Max.x - 0.5f, rc.Max.y), edge, 1.0f);
|
||||
if (draw_bottom) painter->AddLine(ImVec2(rc.Min.x, rc.Max.y - 0.5f), ImVec2(rc.Max.x, rc.Max.y - 0.5f), edge, 1.0f);
|
||||
if (draw_top) painter->AddLine(ImVec2(rc.Min.x, rc.Min.y + 0.5f), ImVec2(rc.Max.x, rc.Min.y + 0.5f), edge, 1.0f);
|
||||
|
||||
|
||||
for (auto &r : subtract) {
|
||||
painter->AddRect(r.Min, r.Max, edge, 0.0f, 0, 2.0f);
|
||||
}
|
||||
painter->PopClipRect();
|
||||
}
|
||||
}
|
||||
98
iqpilot/tools/cabana/ui/widgets/binaryview.h
Normal file
98
iqpilot/tools/cabana/ui/widgets/binaryview.h
Normal file
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
struct BinaryIndex {
|
||||
int row = -1;
|
||||
int column = -1;
|
||||
bool isValid() const { return row >= 0 && column >= 0; }
|
||||
bool operator==(const BinaryIndex &o) const { return row == o.row && column == o.column; }
|
||||
bool operator<(const BinaryIndex &o) const { return std::tie(row, column) < std::tie(o.row, o.column); }
|
||||
};
|
||||
|
||||
class BinaryView {
|
||||
public:
|
||||
static constexpr int COLUMN_COUNT = 9;
|
||||
static constexpr int HEX_COLUMN = 8;
|
||||
|
||||
BinaryView();
|
||||
void setMessage(const MessageId &message_id);
|
||||
void highlight(const cabana::Signal *sig);
|
||||
std::set<const cabana::Signal *> getOverlappingSignals() const;
|
||||
void updateState();
|
||||
|
||||
void draw();
|
||||
ImVec2 minimumSizeHint() const;
|
||||
void setHeatmapLiveMode(bool live) { heatmap_live_mode_ = live; updateState(); }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const cabana::Signal *> signalClicked;
|
||||
Observable<const cabana::Signal *> signalHovered;
|
||||
Observable<const cabana::Signal *, cabana::Signal &> editSignal;
|
||||
Observable<const MessageId &, const cabana::Signal *, bool, bool> showChart;
|
||||
|
||||
private:
|
||||
struct Cell {
|
||||
CabanaColor bg_color = CabanaColor(102, 86, 169, 255);
|
||||
bool is_msb = false;
|
||||
bool is_lsb = false;
|
||||
uint8_t val;
|
||||
std::vector<const cabana::Signal *> sigs;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
void refresh();
|
||||
void setCell(int row, int col, uint8_t val, const CabanaColor &color);
|
||||
const std::vector<std::array<uint32_t, 8>> &bitFlipChanges(size_t msg_size);
|
||||
Cell &cellAt(const BinaryIndex &index) { return cells_[index.row * COLUMN_COUNT + index.column]; }
|
||||
const Cell &cellAt(const BinaryIndex &index) const { return cells_[index.row * COLUMN_COUNT + index.column]; }
|
||||
|
||||
void addShortcuts();
|
||||
std::tuple<int, int, bool> getSelection(BinaryIndex index);
|
||||
void setSelection();
|
||||
void handleMousePress(const ImVec2 &pos);
|
||||
void handleMouseMove(const ImVec2 &pos);
|
||||
void handleMouseRelease(const ImVec2 &pos);
|
||||
void highlightPosition(const ImVec2 &pt);
|
||||
BinaryIndex indexAt(const ImVec2 &pos) const;
|
||||
ImRect visualRect(const BinaryIndex &index) const;
|
||||
bool hasSelection() const { return !selection_.empty(); }
|
||||
bool isSelected(const BinaryIndex &index) const { return selection_.count(index) > 0; }
|
||||
|
||||
void paintCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index) const;
|
||||
bool hasSignal(const BinaryIndex &index, int dx, int dy, const cabana::Signal *sig) const;
|
||||
void drawSignalCell(ImDrawList *painter, const ImRect &rect, const BinaryIndex &index, const cabana::Signal *sig) const;
|
||||
|
||||
MessageId msg_id_;
|
||||
std::vector<Cell> cells_;
|
||||
int row_count_ = 0;
|
||||
bool heatmap_live_mode_ = true;
|
||||
struct BitFlipTracker {
|
||||
std::optional<std::pair<double, double>> time_range;
|
||||
std::vector<std::array<uint32_t, 8>> flip_counts;
|
||||
} bit_flip_tracker_;
|
||||
|
||||
BinaryIndex anchor_index_;
|
||||
ImVec2 last_mouse_pos_{-1, -1};
|
||||
bool is_message_active_ = false;
|
||||
const cabana::Signal *resize_sig_ = nullptr;
|
||||
const cabana::Signal *hovered_sig_ = nullptr;
|
||||
std::set<BinaryIndex> selection_;
|
||||
ImVec2 grid_pos_;
|
||||
float column_width_ = 0;
|
||||
bool under_mouse_ = false;
|
||||
bool scroll_to_top_ = false;
|
||||
Connections connections_;
|
||||
};
|
||||
176
iqpilot/tools/cabana/ui/widgets/cameraview.cc
Normal file
176
iqpilot/tools/cabana/ui/widgets/cameraview.cc
Normal file
@@ -0,0 +1,176 @@
|
||||
#include "tools/cabana/ui/widgets/cameraview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
|
||||
#include "common/yuv.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
constexpr GLenum GL_LINEAR_MIPMAP_LINEAR_ = 0x2703;
|
||||
|
||||
void generateMipmap() {
|
||||
static auto fn = (void (*)(GLenum))glfwGetProcAddress("glGenerateMipmap");
|
||||
if (fn) fn(GL_TEXTURE_2D);
|
||||
}
|
||||
}
|
||||
|
||||
void GlTexture::upload(const RgbImage &image) {
|
||||
if (id == 0) {
|
||||
glGenTextures(1, &id);
|
||||
glBindTexture(GL_TEXTURE_2D, id);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, mipmap ? GL_LINEAR_MIPMAP_LINEAR_ : GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
} else {
|
||||
glBindTexture(GL_TEXTURE_2D, id);
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
if (width != image.width || height != image.height) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
width = image.width;
|
||||
height = image.height;
|
||||
} else {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, image.data.data());
|
||||
}
|
||||
if (mipmap) generateMipmap();
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void GlTexture::destroy() {
|
||||
if (id != 0) {
|
||||
glDeleteTextures(1, &id);
|
||||
}
|
||||
id = 0;
|
||||
width = height = 0;
|
||||
key = 0;
|
||||
}
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type)
|
||||
: stream_name_(stream_name), active_stream_type_(type), requested_stream_type_(type) {}
|
||||
|
||||
CameraWidget::~CameraWidget() {
|
||||
stopVipcThread();
|
||||
}
|
||||
|
||||
void CameraWidget::startVipcThread() {
|
||||
if (!vipc_thread_.joinable()) {
|
||||
clearFrames();
|
||||
vipc_exit_ = false;
|
||||
vipc_thread_ = std::thread(&CameraWidget::vipcThread, this);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::stopVipcThread() {
|
||||
vipc_exit_ = true;
|
||||
if (vipc_thread_.joinable()) {
|
||||
vipc_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::setVisible(bool visible) {
|
||||
if (visible == visible_) return;
|
||||
visible_ = visible;
|
||||
visible ? startVipcThread() : stopVipcThread();
|
||||
}
|
||||
|
||||
void CameraWidget::draw(const ImVec2 &size) {
|
||||
setVisible(true);
|
||||
ImGui::InvisibleButton("##camera", ImVec2(std::max(1.0f, size.x), std::max(1.0f, size.y)),
|
||||
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);
|
||||
rect_ = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
paint();
|
||||
if (ImGui::IsItemDeactivated()) clicked();
|
||||
}
|
||||
|
||||
float CameraWidget::frameAspectRatio() const {
|
||||
if (frame_texture_.width > 0 && frame_texture_.height > 0) {
|
||||
return (float)frame_texture_.width / frame_texture_.height;
|
||||
}
|
||||
return 1928.0f / 1208.0f;
|
||||
}
|
||||
|
||||
void CameraWidget::paint() {
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
p->AddRectFilled(rect_.Min, rect_.Max, bg_);
|
||||
|
||||
std::lock_guard lk(frame_lock_);
|
||||
if (rgb_frame_.isNull()) return;
|
||||
if (frame_updated_) {
|
||||
frame_texture_.upload(rgb_frame_);
|
||||
frame_updated_ = false;
|
||||
}
|
||||
|
||||
|
||||
float widget_ratio = (float)width() / height();
|
||||
float frame_ratio = (float)rgb_frame_.width / rgb_frame_.height;
|
||||
int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f));
|
||||
int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f));
|
||||
ImVec2 video_min(rect_.Min.x + (int)(width() - w) / 2, rect_.Min.y + (int)(height() - h) / 2);
|
||||
ImVec2 video_max(video_min.x + w, video_min.y + h);
|
||||
|
||||
ImVec2 uv0(0, 0), uv1(1, 1);
|
||||
if (active_stream_type_ == VISION_STREAM_DRIVER) {
|
||||
|
||||
uv0.x = 1;
|
||||
uv1.x = 0;
|
||||
}
|
||||
p->AddImage(frame_texture_.ref(), video_min, video_max, uv0, uv1);
|
||||
}
|
||||
|
||||
void CameraWidget::vipcThread() {
|
||||
VisionStreamType cur_stream = requested_stream_type_;
|
||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
||||
VisionIpcBufExtra frame_meta = {};
|
||||
|
||||
while (!vipc_exit_) {
|
||||
if (!vipc_client || cur_stream != requested_stream_type_) {
|
||||
clearFrames();
|
||||
cur_stream = requested_stream_type_;
|
||||
vipc_client.reset(new VisionIpcClient(stream_name_, cur_stream, false));
|
||||
}
|
||||
active_stream_type_ = cur_stream;
|
||||
|
||||
if (!vipc_client->connected) {
|
||||
clearFrames();
|
||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name_, false);
|
||||
if (streams.empty()) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
utils::runOnMainThread(utils::guarded(alive_, [this, streams]() { availableStreamsUpdated(streams); }));
|
||||
|
||||
if (!vipc_client->connect(false)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) {
|
||||
|
||||
if (rgb_back_.width != (int)buf->width || rgb_back_.height != (int)buf->height) {
|
||||
rgb_back_.resize(buf->width, buf->height);
|
||||
}
|
||||
yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride,
|
||||
rgb_back_.data.data(), rgb_back_.bytesPerLine(), buf->width, buf->height);
|
||||
{
|
||||
std::lock_guard lk(frame_lock_);
|
||||
rgb_frame_.swap(rgb_back_);
|
||||
frame_updated_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::clearFrames() {
|
||||
std::lock_guard lk(frame_lock_);
|
||||
rgb_frame_.reset();
|
||||
rgb_back_.reset();
|
||||
frame_updated_ = false;
|
||||
}
|
||||
88
iqpilot/tools/cabana/ui/widgets/cameraview.h
Normal file
88
iqpilot/tools/cabana/ui/widgets/cameraview.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "cereal/visionstream.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
|
||||
|
||||
struct RgbImage {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> data;
|
||||
bool isNull() const { return data.empty(); }
|
||||
void reset() { width = height = 0; data.clear(); }
|
||||
void resize(int w, int h) { width = w; height = h; data.resize(size_t(w) * h * 4); }
|
||||
int bytesPerLine() const { return width * 4; }
|
||||
void swap(RgbImage &other) { std::swap(width, other.width); std::swap(height, other.height); data.swap(other.data); }
|
||||
};
|
||||
|
||||
|
||||
struct GlTexture {
|
||||
GlTexture() = default;
|
||||
GlTexture(const GlTexture &) = delete;
|
||||
GlTexture &operator=(const GlTexture &) = delete;
|
||||
~GlTexture() { destroy(); }
|
||||
void upload(const RgbImage &image);
|
||||
void destroy();
|
||||
ImTextureRef ref() const { return ImTextureRef((ImTextureID)(uintptr_t)id); }
|
||||
|
||||
unsigned int id = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t key = 0;
|
||||
bool mipmap = false;
|
||||
};
|
||||
|
||||
class CameraWidget {
|
||||
public:
|
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type);
|
||||
~CameraWidget();
|
||||
void setStreamType(VisionStreamType type) { requested_stream_type_ = type; }
|
||||
void stopVipcThread();
|
||||
|
||||
|
||||
void setVisible(bool visible);
|
||||
|
||||
void draw(const ImVec2 &size);
|
||||
const ImRect &rect() const { return rect_; }
|
||||
float frameAspectRatio() const;
|
||||
float width() const { return rect_.GetWidth(); }
|
||||
float height() const { return rect_.GetHeight(); }
|
||||
|
||||
Observable<> clicked;
|
||||
Observable<std::set<VisionStreamType>> availableStreamsUpdated;
|
||||
|
||||
private:
|
||||
void paint();
|
||||
void startVipcThread();
|
||||
void vipcThread();
|
||||
void clearFrames();
|
||||
|
||||
ImU32 bg_ = IM_COL32(0, 0, 0, 255);
|
||||
RgbImage rgb_frame_;
|
||||
RgbImage rgb_back_;
|
||||
bool frame_updated_ = false;
|
||||
GlTexture frame_texture_;
|
||||
ImRect rect_;
|
||||
bool visible_ = false;
|
||||
|
||||
std::string stream_name_;
|
||||
std::atomic<VisionStreamType> active_stream_type_;
|
||||
std::atomic<VisionStreamType> requested_stream_type_;
|
||||
std::thread vipc_thread_;
|
||||
std::atomic<bool> vipc_exit_ = false;
|
||||
std::mutex frame_lock_;
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
439
iqpilot/tools/cabana/ui/widgets/detailwidget.cc
Normal file
439
iqpilot/tools/cabana/ui/widgets/detailwidget.cc
Normal file
@@ -0,0 +1,439 @@
|
||||
#include "tools/cabana/ui/widgets/detailwidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool iequals(const std::string &a, const std::string &b) {
|
||||
return a.size() == b.size() &&
|
||||
std::equal(a.begin(), a.end(), b.begin(), [](char x, char y) { return std::tolower((unsigned char)x) == std::tolower((unsigned char)y); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ElidedLabel::ElidedLabel(const std::string &text) : text_(utils::trimmed(text)) {}
|
||||
|
||||
void ElidedLabel::draw(float width) {
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
|
||||
const ImRect bb(pos, ImVec2(pos.x + width, pos.y + ImGui::GetTextLineHeight()));
|
||||
ImGui::ItemSize(bb.GetSize(), 0.0f);
|
||||
if (ImGui::ItemAdd(bb, 0)) {
|
||||
ImGui::RenderTextEllipsis(window->DrawList, bb.Min, bb.Max, bb.Max.x, text_.c_str(), nullptr, nullptr);
|
||||
}
|
||||
if (!tooltip_.empty()) ImGui::SetItemTooltip("%s", tooltip_.c_str());
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||
clicked();
|
||||
}
|
||||
}
|
||||
|
||||
DetailWidget::DetailWidget(ChartsWidget *charts) : charts_(charts) {
|
||||
tabbar_.setUsesScrollButtons(true);
|
||||
tabbar_.setAutoHide(true);
|
||||
tabbar_.setTabsClosable(true);
|
||||
connections_.push_back(tabbar_.currentChanged.connect([this](int index) {
|
||||
if (index >= 0) setMessage(MessageId::fromString(tabbar_.tabText(index)));
|
||||
}));
|
||||
connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { tabbar_.removeTab(index); }));
|
||||
connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) { showTabBarContextMenu(index); }));
|
||||
binary_view_ = std::make_unique<BinaryView>();
|
||||
signal_view_ = std::make_unique<SignalView>(charts);
|
||||
|
||||
history_log_ = std::make_unique<LogsWidget>();
|
||||
|
||||
connections_.push_back(binary_view_->signalHovered.connect([this](const cabana::Signal *s) { signal_view_->signalHovered(s); }));
|
||||
connections_.push_back(binary_view_->signalClicked.connect([this](const cabana::Signal *s) { signal_view_->selectSignal(s, true); }));
|
||||
connections_.push_back(binary_view_->editSignal.connect([this](const cabana::Signal *origin_s, cabana::Signal &s) { signal_view_->saveSignal(origin_s, s); }));
|
||||
connections_.push_back(binary_view_->showChart.connect([this](const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { charts_->showChart(id, sig, show, merge); }));
|
||||
connections_.push_back(signal_view_->showChart.connect([this](const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { charts_->showChart(id, sig, show, merge); }));
|
||||
connections_.push_back(signal_view_->highlight.connect([this](const cabana::Signal *sig) { binary_view_->highlight(sig); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(charts->seriesChanged.connect([this]() { signal_view_->updateChartState(); }));
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const std::optional<std::pair<double, double>> &range) {
|
||||
char text[64];
|
||||
if (range) snprintf(text, sizeof(text), "%.3f - %.3f", range->first, range->second);
|
||||
heatmap_all_text_ = range ? text : "All";
|
||||
const bool live = !range;
|
||||
if (std::exchange(heatmap_live_, live) != live) binary_view_->setHeatmapLiveMode(live);
|
||||
}));
|
||||
}
|
||||
|
||||
void DetailWidget::drawToolBar() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
auto radio_width = [&](const char *label) { return ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(label).x; };
|
||||
auto button_width = [&](const char *label) { return ImGui::CalcTextSize(label).x + style.FramePadding.x * 2; };
|
||||
const float right_width = ImGui::CalcTextSize("Heatmap:").x + style.ItemSpacing.x + radio_width("Live") + style.ItemSpacing.x +
|
||||
radio_width(heatmap_all_text_.c_str()) + style.ItemSpacing.x * 3 + 1.0f +
|
||||
button_width(icon::PENCIL) + style.ItemSpacing.x + button_width(icon::X_LG);
|
||||
const float avail = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
pushBoldFont();
|
||||
name_label_.draw(std::max(1.0f, avail - right_width - style.ItemSpacing.x));
|
||||
popBoldFont();
|
||||
|
||||
alignRight(right_width);
|
||||
ImGui::TextUnformatted("Heatmap:");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton("Live##heatmap_live_", heatmap_live_) && !heatmap_live_) {
|
||||
heatmap_live_ = true;
|
||||
binary_view_->setHeatmapLiveMode(true);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::RadioButton((heatmap_all_text_ + "##heatmap_all").c_str(), !heatmap_live_) && heatmap_live_) {
|
||||
heatmap_live_ = false;
|
||||
binary_view_->setHeatmapLiveMode(false);
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(icon::PENCIL)) editMsg();
|
||||
ImGui::SetItemTooltip("Edit Message");
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!action_remove_msg_enabled_);
|
||||
if (ImGui::Button(icon::X_LG)) UndoStack::instance()->push(new RemoveMsgCommand(msg_id_));
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Remove Message");
|
||||
}
|
||||
|
||||
void DetailWidget::showTabBarContextMenu(int index) {
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Close Other Tabs")) {
|
||||
tabbar_.moveTab(index, 0);
|
||||
tabbar_.setCurrentIndex(0);
|
||||
while (tabbar_.count() > 1) tabbar_.removeTab(1);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
int DetailWidget::findOrAddTab(const MessageId &message_id) {
|
||||
const std::string text = message_id.toString();
|
||||
int index = tabbar_.count() - 1;
|
||||
for ( ; index >= 0; --index) {
|
||||
if (tabbar_.tabText(index) == text) break;
|
||||
}
|
||||
if (index == -1) {
|
||||
index = tabbar_.addTab(text);
|
||||
tabbar_.setTabToolTip(index, msgName(message_id));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void DetailWidget::setMessage(const MessageId &message_id) {
|
||||
if (std::exchange(msg_id_, message_id) == message_id) return;
|
||||
|
||||
tabbar_.setCurrentIndex(findOrAddTab(message_id));
|
||||
|
||||
signal_view_->setMessage(msg_id_);
|
||||
binary_view_->setMessage(msg_id_);
|
||||
history_log_->setMessage(msg_id_);
|
||||
refresh();
|
||||
}
|
||||
|
||||
std::pair<std::string, std::vector<std::string>> DetailWidget::serializeMessageIds() const {
|
||||
std::vector<std::string> msgs;
|
||||
for (int i = 0; i < tabbar_.count(); ++i) msgs.push_back(tabbar_.tabText(i));
|
||||
return std::make_pair(msg_id_.toString(), msgs);
|
||||
}
|
||||
|
||||
void DetailWidget::restoreTabs(const std::string &active_msg_id, const std::vector<std::string>& msg_ids) {
|
||||
for (const auto& str_id : msg_ids) {
|
||||
MessageId id = MessageId::fromString(str_id);
|
||||
if (dbc()->msg(id) != nullptr)
|
||||
findOrAddTab(id);
|
||||
}
|
||||
|
||||
auto active_id = MessageId::fromString(active_msg_id);
|
||||
if (dbc()->msg(active_id) != nullptr)
|
||||
setMessage(active_id);
|
||||
}
|
||||
|
||||
void DetailWidget::refresh() {
|
||||
std::vector<std::string> warnings;
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
if (msg) {
|
||||
if (msg_id_.source == INVALID_SOURCE) {
|
||||
warnings.push_back("No messages received.");
|
||||
} else if (msg->size != can->lastMessage(msg_id_).dat.size()) {
|
||||
warnings.push_back("Message size (" + std::to_string(msg->size) + ") is incorrect.");
|
||||
}
|
||||
for (auto s : binary_view_->getOverlappingSignals()) {
|
||||
warnings.push_back(s->name + " has overlapping bits.");
|
||||
}
|
||||
}
|
||||
std::string msg_name = msg ? msg->name + " (" + msg->transmitter + ")" : msgName(msg_id_);
|
||||
name_label_.setText(msg_name);
|
||||
name_label_.setToolTip(msg_name);
|
||||
action_remove_msg_enabled_ = msg != nullptr;
|
||||
|
||||
if (!warnings.empty()) {
|
||||
warning_label_.clear();
|
||||
for (size_t i = 0; i < warnings.size(); ++i) {
|
||||
if (i) warning_label_ += '\n';
|
||||
warning_label_ += warnings[i];
|
||||
}
|
||||
warning_icon_ = msg ? icon::EXCLAMATION_TRIANGLE : icon::INFO_CIRCLE;
|
||||
}
|
||||
warning_widget_visible_ = !warnings.empty();
|
||||
}
|
||||
|
||||
void DetailWidget::updateState(const std::set<MessageId> *msgs) {
|
||||
if ((msgs && !msgs->count(msg_id_)))
|
||||
return;
|
||||
|
||||
if (tab_widget_index_ == 0)
|
||||
binary_view_->updateState();
|
||||
else
|
||||
history_log_->updateState();
|
||||
}
|
||||
|
||||
void DetailWidget::editMsg() {
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
int size = msg ? msg->size : can->lastMessage(msg_id_).dat.size();
|
||||
edit_dlg_ = std::make_unique<EditMessageDialog>(msg_id_, msgName(msg_id_), size, ImGui::GetWindowWidth());
|
||||
}
|
||||
|
||||
void DetailWidget::drawTabWidget() {
|
||||
|
||||
const float tab_height = ImGui::GetFrameHeight();
|
||||
const float content_height = ImGui::GetContentRegionAvail().y - tab_height - ImGui::GetStyle().ItemSpacing.y;
|
||||
ImGui::BeginChild("tab_widget", ImVec2(0, std::max(content_height, 1.0f)), ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
if (tab_widget_index_ == 0) {
|
||||
|
||||
const float min_height = binary_view_->minimumSizeHint().y;
|
||||
const float avail = ImGui::GetContentRegionAvail().y;
|
||||
const float max_height = std::max(avail - 6.0f - ImGui::GetStyle().ItemSpacing.y * 2 - 1.0f, 1.0f);
|
||||
const float height = std::clamp(min_height, 1.0f, max_height);
|
||||
ImGui::BeginChild("binary_view", ImVec2(0, height));
|
||||
binary_view_rect_ = ImGui::GetCurrentWindow()->Rect();
|
||||
binary_view_->draw();
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0.0f, 6.0f));
|
||||
const float spacing = ImGui::GetStyle().ItemSpacing.y;
|
||||
const ImRect child_rect = ImGui::GetCurrentWindow()->Rect();
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(child_rect.Min.x, ImGui::GetItemRectMin().y - spacing),
|
||||
ImVec2(child_rect.Max.x, ImGui::GetItemRectMax().y + spacing),
|
||||
ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
ImGui::BeginChild("signal_view", ImVec2(0, 0));
|
||||
signal_view_rect_ = ImGui::GetCurrentWindow()->Rect();
|
||||
signal_view_->draw();
|
||||
ImGui::EndChild();
|
||||
} else {
|
||||
history_log_->draw();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
const std::string labels[] = {std::string(icon::FILE_EARMARK_RULED) + " Messages", std::string(icon::STOPWATCH) + " Logs"};
|
||||
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
float tabs_width = 0.0f;
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
tabs_width += ImGui::TabItemCalcSize(labels[i].c_str(), false).x + (i ? style.ItemInnerSpacing.x : 0.0f);
|
||||
}
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
const float separator_y = ImGui::GetCursorScreenPos().y + ImGui::GetFrameHeight() - 1.0f;
|
||||
window->DrawList->AddLine(ImVec2(window->WorkRect.Min.x, separator_y), ImVec2(window->WorkRect.Max.x, separator_y),
|
||||
ImGui::GetColorU32(ImGuiCol_TabSelected), style.TabBarBorderSize);
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + std::max(0.0f, (ImGui::GetContentRegionAvail().x - tabs_width) * 0.5f));
|
||||
|
||||
if (ImGui::BeginTabBar("tab_widget_tabs")) {
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
if (ImGui::BeginTabItem(labels[i].c_str())) {
|
||||
if (tab_widget_index_ != i) {
|
||||
tab_widget_index_ = i;
|
||||
if (i == 1) history_log_->onShown();
|
||||
updateState();
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
void DetailWidget::draw() {
|
||||
tabbar_.draw();
|
||||
drawToolBar();
|
||||
|
||||
if (warning_widget_visible_) {
|
||||
ImGui::TextUnformatted(warning_icon_);
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(warning_label_.c_str());
|
||||
}
|
||||
|
||||
drawTabWidget();
|
||||
|
||||
if (edit_dlg_ && !edit_dlg_->draw()) {
|
||||
if (edit_dlg_->accepted()) {
|
||||
const auto r = edit_dlg_->result();
|
||||
UndoStack::instance()->push(new EditMsgCommand(r.msg_id, r.name, r.size, r.node, r.comment));
|
||||
}
|
||||
edit_dlg_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::vector<std::pair<std::string, ImRect>> DetailWidget::helpRects() const {
|
||||
std::vector<std::pair<std::string, ImRect>> rects;
|
||||
if (tab_widget_index_ == 0) {
|
||||
rects.emplace_back(binary_view_->whatsThis(), binary_view_rect_);
|
||||
rects.emplace_back(signal_view_->whatsThis(), signal_view_rect_);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
EditMessageDialog::EditMessageDialog(const MessageId &msg_id, const std::string &title, int size, float parent_width)
|
||||
: msg_id_(msg_id), original_name_(title), name_edit_(title), size_spin_(size), width_(parent_width * 0.9f) {
|
||||
window_title_ = "Edit message: " + msg_id.toString();
|
||||
|
||||
if (auto msg = dbc()->msg(msg_id)) {
|
||||
node_ = msg->transmitter;
|
||||
comment_edit_ = msg->comment;
|
||||
}
|
||||
validateName(name_edit_);
|
||||
}
|
||||
|
||||
EditMessageDialog::Result EditMessageDialog::result() const {
|
||||
return {msg_id_, utils::trimmed(name_edit_), utils::trimmed(node_), utils::trimmed(comment_edit_), size_spin_};
|
||||
}
|
||||
|
||||
bool EditMessageDialog::draw() {
|
||||
if (closed_) return false;
|
||||
if (!opened_) {
|
||||
ImGui::OpenPopup(window_title_.c_str());
|
||||
opened_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(0.0f, 0.0f));
|
||||
ImGui::SetNextWindowSize(ImVec2(width_, 0.0f), ImGuiCond_Always);
|
||||
bool open = true;
|
||||
if (ImGui::BeginPopupModal(window_title_.c_str(), &open)) {
|
||||
const float label_width = ImGui::CalcTextSize("Comment").x + ImGui::GetStyle().ItemSpacing.x * 2;
|
||||
auto row = [&](const char *label) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(label);
|
||||
ImGui::SameLine(label_width);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
};
|
||||
|
||||
if (!error_label_.empty()) {
|
||||
row("");
|
||||
ImGui::TextUnformatted(error_label_.c_str());
|
||||
}
|
||||
row("Name");
|
||||
if (validatedInput("##name", &name_edit_, nameValidator)) {
|
||||
validateName(name_edit_);
|
||||
}
|
||||
|
||||
row("Size");
|
||||
if (ImGui::InputInt("##size", &size_spin_)) size_spin_ = std::clamp(size_spin_, 1, CAN_MAX_DATA_BYTES);
|
||||
|
||||
row("Node");
|
||||
validatedInput("##node", &node_, nameValidator);
|
||||
row("Comment");
|
||||
inputTextMultiline("##comment", &comment_edit_, ImVec2(-FLT_MIN, 192.0f));
|
||||
const bool comment_active = ImGui::IsItemActive();
|
||||
|
||||
bool accept = false, reject = false;
|
||||
if (dialogButtons("OK", &accept, &reject, ok_enabled_)) {
|
||||
accepted_ = accept;
|
||||
closed_ = true;
|
||||
}
|
||||
|
||||
if (!closed_ && ok_enabled_ && !comment_active && ImGui::IsKeyPressed(ImGuiKey_Enter, false)) {
|
||||
accepted_ = true;
|
||||
closed_ = true;
|
||||
}
|
||||
if (!open) closed_ = true;
|
||||
if (closed_) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
} else {
|
||||
closed_ = true;
|
||||
}
|
||||
return !closed_;
|
||||
}
|
||||
|
||||
void EditMessageDialog::validateName(const std::string &text) {
|
||||
bool valid = !iequals(text, UNTITLED);
|
||||
error_label_.clear();
|
||||
if (!text.empty() && valid && text != original_name_) {
|
||||
valid = dbc()->msg(msg_id_.source, text) == nullptr;
|
||||
if (!valid) error_label_ = "Name already exists";
|
||||
}
|
||||
ok_enabled_ = valid;
|
||||
}
|
||||
|
||||
DetailWidget* CenterWidget::ensureDetailWidget() {
|
||||
if (!detail_widget) {
|
||||
detail_widget = std::make_unique<DetailWidget>(charts_);
|
||||
}
|
||||
return detail_widget.get();
|
||||
}
|
||||
|
||||
void CenterWidget::clear() {
|
||||
detail_widget.reset();
|
||||
charts_ = nullptr;
|
||||
}
|
||||
|
||||
void CenterWidget::draw() {
|
||||
if (detail_widget) {
|
||||
detail_widget->draw();
|
||||
} else {
|
||||
drawWelcomeWidget();
|
||||
}
|
||||
}
|
||||
|
||||
void CenterWidget::drawWelcomeWidget() {
|
||||
const ImVec2 win_pos = ImGui::GetWindowPos(), win_size = ImGui::GetWindowSize();
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(win_pos, ImVec2(win_pos.x + win_size.x, win_pos.y + win_size.y), ImGui::GetColorU32(ImGuiCol_ChildBg));
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const ImVec2 origin = ImGui::GetCursorPos();
|
||||
auto centered = [&](const char *text, float y) {
|
||||
const ImVec2 size = ImGui::CalcTextSize(text);
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - size.x) * 0.5f, y));
|
||||
ImGui::TextUnformatted(text);
|
||||
};
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colorRgb(169, 169, 169));
|
||||
float y = origin.y + avail.y * 0.5f - 90.0f;
|
||||
pushLargeFont();
|
||||
centered("CABANA", y);
|
||||
y += ImGui::GetTextLineHeightWithSpacing();
|
||||
popLargeFont();
|
||||
|
||||
auto newShortcutRow = [&](const char *title, const char *key) {
|
||||
const float w = ImGui::CalcTextSize(title).x + ImGui::CalcTextSize(key).x + 40.0f;
|
||||
ImGui::SetCursorPos(ImVec2(origin.x + (avail.x - w) * 0.5f, y));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(title);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::SmallButton(key);
|
||||
ImGui::EndDisabled();
|
||||
y += ImGui::GetFrameHeightWithSpacing();
|
||||
};
|
||||
|
||||
centered("<-Select a message to view details", y);
|
||||
y += ImGui::GetTextLineHeightWithSpacing();
|
||||
newShortcutRow("Pause", "Space");
|
||||
newShortcutRow("Help", "F1");
|
||||
newShortcutRow("WhatsThis", "Shift+F1");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
112
iqpilot/tools/cabana/ui/widgets/detailwidget.h
Normal file
112
iqpilot/tools/cabana/ui/widgets/detailwidget.h
Normal file
@@ -0,0 +1,112 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/ui/widgets/binaryview.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/widgets/historylog.h"
|
||||
#include "tools/cabana/ui/widgets/signalview.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
|
||||
|
||||
class ElidedLabel {
|
||||
public:
|
||||
explicit ElidedLabel(const std::string &text = {});
|
||||
void setText(const std::string &text) { text_ = text; }
|
||||
void setToolTip(const std::string &tip) { tooltip_ = tip; }
|
||||
void draw(float width);
|
||||
|
||||
Observable<> clicked;
|
||||
|
||||
private:
|
||||
std::string text_, tooltip_;
|
||||
};
|
||||
|
||||
|
||||
class EditMessageDialog {
|
||||
public:
|
||||
struct Result {
|
||||
MessageId msg_id;
|
||||
std::string name, node, comment;
|
||||
int size;
|
||||
};
|
||||
|
||||
EditMessageDialog(const MessageId &msg_id, const std::string &title, int size, float parent_width);
|
||||
bool draw();
|
||||
bool accepted() const { return accepted_; }
|
||||
Result result() const;
|
||||
|
||||
private:
|
||||
void validateName(const std::string &text);
|
||||
|
||||
MessageId msg_id_;
|
||||
std::string original_name_;
|
||||
std::string name_edit_;
|
||||
std::string node_;
|
||||
std::string comment_edit_;
|
||||
std::string error_label_;
|
||||
int size_spin_;
|
||||
bool ok_enabled_ = true;
|
||||
std::string window_title_;
|
||||
float width_;
|
||||
bool opened_ = false;
|
||||
bool accepted_ = false;
|
||||
bool closed_ = false;
|
||||
};
|
||||
|
||||
class DetailWidget {
|
||||
public:
|
||||
DetailWidget(ChartsWidget *charts);
|
||||
void setMessage(const MessageId &message_id);
|
||||
void refresh();
|
||||
void draw();
|
||||
std::pair<std::string, std::vector<std::string>> serializeMessageIds() const;
|
||||
void restoreTabs(const std::string &active_msg_id, const std::vector<std::string> &msg_ids);
|
||||
std::vector<std::pair<std::string, ImRect>> helpRects() const;
|
||||
|
||||
private:
|
||||
void drawToolBar();
|
||||
void drawTabWidget();
|
||||
int findOrAddTab(const MessageId& message_id);
|
||||
void showTabBarContextMenu(int index);
|
||||
void editMsg();
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
MessageId msg_id_;
|
||||
const char *warning_icon_ = nullptr;
|
||||
std::string warning_label_;
|
||||
ElidedLabel name_label_;
|
||||
bool warning_widget_visible_ = false;
|
||||
TabBar tabbar_;
|
||||
int tab_widget_index_ = 0;
|
||||
bool action_remove_msg_enabled_ = false;
|
||||
bool heatmap_live_ = true;
|
||||
std::string heatmap_all_text_ = "All";
|
||||
ImRect binary_view_rect_, signal_view_rect_;
|
||||
std::unique_ptr<LogsWidget> history_log_;
|
||||
std::unique_ptr<BinaryView> binary_view_;
|
||||
std::unique_ptr<SignalView> signal_view_;
|
||||
ChartsWidget *charts_;
|
||||
std::unique_ptr<EditMessageDialog> edit_dlg_;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class CenterWidget {
|
||||
public:
|
||||
CenterWidget() = default;
|
||||
void setChartsWidget(ChartsWidget *charts) { charts_ = charts; }
|
||||
void setMessage(const MessageId &message_id) { ensureDetailWidget()->setMessage(message_id); }
|
||||
DetailWidget* getDetailWidget() { return detail_widget.get(); }
|
||||
DetailWidget* ensureDetailWidget();
|
||||
void clear();
|
||||
void draw();
|
||||
|
||||
private:
|
||||
void drawWelcomeWidget();
|
||||
std::unique_ptr<DetailWidget> detail_widget;
|
||||
ChartsWidget *charts_ = nullptr;
|
||||
};
|
||||
307
iqpilot/tools/cabana/ui/widgets/historylog.cc
Normal file
307
iqpilot/tools/cabana/ui/widgets/historylog.cc
Normal file
@@ -0,0 +1,307 @@
|
||||
#include "tools/cabana/ui/widgets/historylog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <iterator>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/dialogs/filedialog.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
#include "tools/cabana/utils/export.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int BATCH_SIZE = 50;
|
||||
constexpr float DISPLAY_TYPE_WIDTH = 90.0f;
|
||||
constexpr float SIGNALS_WIDTH = 160.0f;
|
||||
constexpr float COMPARE_WIDTH = 50.0f;
|
||||
|
||||
std::string formatTime(uint64_t mono_time) {
|
||||
char buf[32] = {};
|
||||
snprintf(buf, sizeof(buf), "%.3f", can->toSeconds(mono_time));
|
||||
return buf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LogsWidget::LogsWidget() {
|
||||
connections_.push_back(can->seekedTo.connect([this](double) { reset(); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); }));
|
||||
}
|
||||
|
||||
void LogsWidget::setMessage(const MessageId &message_id) {
|
||||
msg_id_ = message_id;
|
||||
reset();
|
||||
}
|
||||
|
||||
void LogsWidget::reset() {
|
||||
sigs_.clear();
|
||||
if (auto dbc_msg = dbc()->msg(msg_id_)) sigs_ = dbc_msg->getSignals();
|
||||
messages_.clear();
|
||||
hex_colors_ = {};
|
||||
signals_cb_ = comp_box_ = 0;
|
||||
value_edit_.clear();
|
||||
value_edit_modified_ = false;
|
||||
export_btn_enabled_ = false;
|
||||
selected_row_ = selected_col_ = -1;
|
||||
setFilter(0, "", nullptr);
|
||||
}
|
||||
|
||||
void LogsWidget::setFilter(int sig_idx, const std::string &value, std::function<bool(double, double)> cmp) {
|
||||
filter_sig_idx_ = sig_idx;
|
||||
filter_value_ = utils::toDouble(value);
|
||||
filter_cmp_ = value.empty() ? nullptr : cmp;
|
||||
load(true);
|
||||
}
|
||||
|
||||
void LogsWidget::load(bool clear) {
|
||||
if (clear && !messages_.empty()) {
|
||||
messages_.clear();
|
||||
selected_row_ = selected_col_ = -1;
|
||||
}
|
||||
const uint64_t current_time = can->toMonoTime(can->lastMessage(msg_id_).ts) + 1;
|
||||
fetch(messages_.begin(), current_time, messages_.empty() ? 0 : messages_.front().mono_time);
|
||||
}
|
||||
|
||||
bool LogsWidget::canFetchMore() const {
|
||||
const auto &events = can->events(msg_id_);
|
||||
return !events.empty() && !messages_.empty() && messages_.back().mono_time > events.front()->mono_time;
|
||||
}
|
||||
|
||||
void LogsWidget::fetch(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time) {
|
||||
const auto &events = can->events(msg_id_);
|
||||
auto first = std::upper_bound(events.rbegin(), events.rend(), from_time, [](uint64_t ts, auto e) { return ts > e->mono_time; });
|
||||
|
||||
std::vector<Message> msgs;
|
||||
std::vector<double> values(sigs_.size());
|
||||
msgs.reserve(BATCH_SIZE);
|
||||
for (; first != events.rend() && (*first)->mono_time > min_time; ++first) {
|
||||
const CanEvent *e = *first;
|
||||
for (int i = 0; i < sigs_.size(); ++i) {
|
||||
sigs_[i]->getValue(e->dat, e->size, &values[i]);
|
||||
}
|
||||
if (!filter_cmp_ || filter_cmp_(values[filter_sig_idx_], filter_value_)) {
|
||||
msgs.emplace_back(Message{e->mono_time, values, {e->dat, e->dat + e->size}});
|
||||
if (msgs.size() >= BATCH_SIZE && min_time == 0) break;
|
||||
}
|
||||
}
|
||||
if (msgs.empty()) return;
|
||||
|
||||
if (hexMode() && (min_time > 0 || messages_.empty())) {
|
||||
const auto freq = can->lastMessage(msg_id_).freq;
|
||||
const std::vector<uint8_t> no_mask;
|
||||
for (auto &m : msgs) {
|
||||
hex_colors_.compute(msg_id_, m.data.data(), m.data.size(), m.mono_time / (double)1e9, can->getSpeed(), no_mask, freq);
|
||||
m.colors = hex_colors_.colors;
|
||||
}
|
||||
}
|
||||
const int pos = std::distance(messages_.begin(), insert_pos);
|
||||
messages_.insert(insert_pos, std::move_iterator(msgs.begin()), std::move_iterator(msgs.end()));
|
||||
export_btn_enabled_ = true;
|
||||
|
||||
if (selected_row_ >= pos) selected_row_ += msgs.size();
|
||||
}
|
||||
|
||||
void LogsWidget::filterChanged() {
|
||||
if (value_edit_.empty() && !value_edit_modified_) return;
|
||||
|
||||
std::function<bool(double, double)> cmp = nullptr;
|
||||
switch (comp_box_) {
|
||||
case 0: cmp = std::greater<double>{}; break;
|
||||
case 1: cmp = std::equal_to<double>{}; break;
|
||||
case 2: cmp = [](double l, double r) { return l != r; }; break;
|
||||
case 3: cmp = std::less<double>{}; break;
|
||||
}
|
||||
setFilter(signals_cb_, value_edit_, cmp);
|
||||
}
|
||||
|
||||
void LogsWidget::exportToCSV() {
|
||||
std::string dir = settings.last_dir + "/" + can->routeName() + "_" + msgName(msg_id_) + ".csv";
|
||||
FileDialog::getSaveFileName("Export " + msgName(msg_id_) + " to CSV file", dir, ".csv", [this](const std::string &fn) {
|
||||
if (!fn.empty()) {
|
||||
hexMode() ? utils::exportToCSV(fn, msg_id_) : utils::exportSignalsToCSV(fn, msg_id_);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void LogsWidget::draw() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
|
||||
|
||||
const float export_w = ImGui::CalcTextSize(icon::FILETYPE_CSV).x + style.FramePadding.x * 2;
|
||||
if (!sigs_.empty()) {
|
||||
const float clear_w = value_edit_.empty() ? 0.0f : ImGui::CalcTextSize(icon::X).x + style.FramePadding.x * 2;
|
||||
const float fixed = DISPLAY_TYPE_WIDTH + SIGNALS_WIDTH + COMPARE_WIDTH + clear_w + style.ItemSpacing.x * 4 + export_w;
|
||||
const float value_w = std::clamp(ImGui::GetContentRegionAvail().x - fixed, 30.0f, 120.0f);
|
||||
|
||||
ImGui::SetNextItemWidth(DISPLAY_TYPE_WIDTH);
|
||||
if (ImGui::Combo("##display_type", &display_type_cb_, "Signal\0Hex\0")) {
|
||||
hex_mode_ = display_type_cb_;
|
||||
reset();
|
||||
}
|
||||
ImGui::SetItemTooltip("Display signal value or raw hex value");
|
||||
ImGui::SameLine();
|
||||
std::string sig_items;
|
||||
for (auto s : sigs_) {
|
||||
sig_items += s->name;
|
||||
sig_items += '\0';
|
||||
}
|
||||
sig_items += '\0';
|
||||
ImGui::SetNextItemWidth(SIGNALS_WIDTH);
|
||||
if (ImGui::Combo("##signals", &signals_cb_, sig_items.c_str())) filterChanged();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(COMPARE_WIDTH);
|
||||
if (ImGui::Combo("##comp", &comp_box_, ">\0=\0!=\0<\0")) filterChanged();
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(value_w);
|
||||
if (clearableInput("##value", &value_edit_, "", doubleValidator)) {
|
||||
value_edit_modified_ = true;
|
||||
filterChanged();
|
||||
}
|
||||
}
|
||||
alignRight(export_w);
|
||||
ImGui::BeginDisabled(!export_btn_enabled_);
|
||||
if (ImGui::Button(icon::FILETYPE_CSV)) exportToCSV();
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Export to CSV file...");
|
||||
|
||||
ImGui::Separator();
|
||||
drawTable();
|
||||
}
|
||||
|
||||
std::string LogsWidget::headerText(int column) const {
|
||||
if (column == 0) return "Time";
|
||||
if (hexMode()) return "Data";
|
||||
std::string text = sigs_[column - 1]->name;
|
||||
if (!sigs_[column - 1]->unit.empty()) text += " (" + sigs_[column - 1]->unit + ")";
|
||||
std::replace(text.begin(), text.end(), '_', ' ');
|
||||
return text;
|
||||
}
|
||||
|
||||
ImVec2 LogsWidget::headerSize(int column, float viewport_width) const {
|
||||
const ImVec2 time_text_size = ImGui::CalcTextSize("000000.000");
|
||||
const ImVec2 time_col_size(time_text_size.x + 10, time_text_size.y + 6);
|
||||
if (column == 0) return time_col_size;
|
||||
const int default_size = std::max(100, (int)((viewport_width - time_col_size.x) / (columnCount() - 1)));
|
||||
const ImVec2 rect = ImGui::CalcTextSize(headerText(column).c_str(), nullptr, false, default_size);
|
||||
return ImVec2{std::max(rect.x + 10, (float)default_size), rect.y + 6};
|
||||
}
|
||||
|
||||
void LogsWidget::drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) const {
|
||||
if (column > 0 && !hexMode()) {
|
||||
CabanaColor bg = sigs_[column - 1]->color;
|
||||
bg.a = 128;
|
||||
dl->AddRectFilled(rect.Min, rect.Max, toImU32(bg));
|
||||
}
|
||||
const std::string text = headerText(column);
|
||||
const ImU32 color = isDarkTheme() ? toImU32(DarkTheme::bright_text) : ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
const ImRect r(rect.Min.x + 5, rect.Min.y + 3, rect.Max.x - 5, rect.Max.y - 3);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
const float wrap_width = std::max(1.0f, r.GetWidth());
|
||||
const char *s = text.c_str();
|
||||
const char *end = s + text.size();
|
||||
float y = r.Min.y;
|
||||
dl->PushClipRect(rect.Min, rect.Max, true);
|
||||
while (s < end) {
|
||||
const char *line_end = font->CalcWordWrapPosition(font_size, s, end, wrap_width);
|
||||
if (line_end == s) line_end = s + 1;
|
||||
const float w = ImGui::CalcTextSize(s, line_end).x;
|
||||
dl->AddText(font, font_size, ImVec2(r.Max.x - w, y), color, s, line_end);
|
||||
y += ImGui::GetTextLineHeight();
|
||||
s = line_end;
|
||||
while (s < end && ImCharIsBlankA(*s)) s++;
|
||||
if (s < end && *s == '\n') s++;
|
||||
}
|
||||
dl->PopClipRect();
|
||||
}
|
||||
|
||||
void LogsWidget::drawTable() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const int cols = columnCount();
|
||||
|
||||
|
||||
const float header_width = ImGui::GetContentRegionAvail().x - style.CellPadding.x * 2 * cols -
|
||||
(vscrollbar_visible_ ? style.ScrollbarSize : 0.0f);
|
||||
|
||||
std::vector<ImVec2> sizes(cols);
|
||||
float header_height = 0;
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
sizes[i] = headerSize(i, header_width);
|
||||
header_height = std::max(header_height, sizes[i].y);
|
||||
}
|
||||
if (hexMode() && !messages_.empty()) {
|
||||
sizes[1].x = std::max(sizes[1].x, bytesCellSize(messages_.front().data.size(), false).x);
|
||||
}
|
||||
const float row_height = bytesCellSize(8, false).y;
|
||||
|
||||
|
||||
|
||||
ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_BordersInner |
|
||||
ImGuiTableFlags_SizingFixedFit;
|
||||
|
||||
if (messages_.empty()) flags &= ~ImGuiTableFlags_BordersInnerV;
|
||||
|
||||
float inner_width = 0;
|
||||
for (int i = 0; i < cols; ++i) inner_width += sizes[i].x + style.CellPadding.x * 2;
|
||||
|
||||
bool fetch_more = false;
|
||||
if (ImGui::BeginTable("logs", cols, flags, ImVec2(0, 0), inner_width)) {
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
ImGui::TableSetupColumn(headerText(i).c_str(), ImGuiTableColumnFlags_WidthFixed, sizes[i].x);
|
||||
}
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
|
||||
ImGui::TableNextRow(ImGuiTableRowFlags_Headers, header_height);
|
||||
for (int i = 0; i < cols; ++i) {
|
||||
if (!ImGui::TableSetColumnIndex(i)) continue;
|
||||
drawHeaderCell(painter, ImGui::TableGetCellBgRect(table, i), i);
|
||||
ImGui::Dummy(ImVec2(0, header_height - style.CellPadding.y * 2));
|
||||
}
|
||||
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(messages_.size(), row_height);
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) {
|
||||
const auto &m = messages_[row];
|
||||
ImGui::TableNextRow(0, row_height);
|
||||
|
||||
ImGui::PushID((void *)(uintptr_t)m.mono_time);
|
||||
for (int col = 0; col < cols; ++col) {
|
||||
if (!ImGui::TableSetColumnIndex(col)) continue;
|
||||
|
||||
const bool cell_selected = selected_row_ == row && selected_col_ == col;
|
||||
ImGui::PushID(col);
|
||||
if (viewSelectable("##cell", cell_selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height - style.CellPadding.y * 2))) {
|
||||
selected_row_ = row;
|
||||
selected_col_ = col;
|
||||
}
|
||||
ImGui::PopID();
|
||||
const ImRect rect = ImGui::TableGetCellBgRect(table, col);
|
||||
if (col == 0) {
|
||||
drawTextCell(painter, rect, formatTime(m.mono_time), cell_selected, false);
|
||||
} else if (hexMode()) {
|
||||
drawBytesCell(painter, rect, m.data, &m.colors, cell_selected, false, false);
|
||||
} else {
|
||||
drawTextCell(painter, rect, sigs_[col - 1]->formatValue(m.sig_values[col - 1], false), cell_selected, false);
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (clipper.DisplayEnd >= (int)messages_.size()) fetch_more = true;
|
||||
}
|
||||
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) fetch_more = true;
|
||||
vscrollbar_visible_ = table->InnerWindow->ScrollbarY;
|
||||
ImGui::EndTable();
|
||||
}
|
||||
if (fetch_more && canFetchMore()) fetch(messages_.end(), messages_.back().mono_time, 0);
|
||||
}
|
||||
59
iqpilot/tools/cabana/ui/widgets/historylog.h
Normal file
59
iqpilot/tools/cabana/ui/widgets/historylog.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
class LogsWidget {
|
||||
public:
|
||||
LogsWidget();
|
||||
void setMessage(const MessageId &message_id);
|
||||
void updateState() { load(false); }
|
||||
void onShown() { load(true); }
|
||||
void draw();
|
||||
|
||||
private:
|
||||
struct Message {
|
||||
uint64_t mono_time = 0;
|
||||
std::vector<double> sig_values;
|
||||
std::vector<uint8_t> data;
|
||||
std::vector<CabanaColor> colors;
|
||||
};
|
||||
|
||||
bool hexMode() const { return sigs_.empty() || hex_mode_; }
|
||||
int columnCount() const { return hexMode() ? 2 : (int)sigs_.size() + 1; }
|
||||
void reset();
|
||||
void setFilter(int sig_idx, const std::string &value, std::function<bool(double, double)> cmp);
|
||||
void load(bool clear);
|
||||
bool canFetchMore() const;
|
||||
void fetch(std::deque<Message>::iterator insert_pos, uint64_t from_time, uint64_t min_time);
|
||||
void filterChanged();
|
||||
void exportToCSV();
|
||||
void drawTable();
|
||||
std::string headerText(int column) const;
|
||||
ImVec2 headerSize(int column, float viewport_width) const;
|
||||
void drawHeaderCell(ImDrawList *dl, const ImRect &rect, int column) const;
|
||||
|
||||
MessageId msg_id_;
|
||||
std::vector<cabana::Signal *> sigs_;
|
||||
std::deque<Message> messages_;
|
||||
CanData hex_colors_;
|
||||
bool hex_mode_ = false;
|
||||
int filter_sig_idx_ = -1;
|
||||
double filter_value_ = 0;
|
||||
std::function<bool(double, double)> filter_cmp_;
|
||||
int signals_cb_ = 0, comp_box_ = 0, display_type_cb_ = 0;
|
||||
std::string value_edit_;
|
||||
bool value_edit_modified_ = false;
|
||||
bool export_btn_enabled_ = false;
|
||||
int selected_row_ = -1, selected_col_ = -1;
|
||||
bool vscrollbar_visible_ = false;
|
||||
Connections connections_;
|
||||
};
|
||||
60
iqpilot/tools/cabana/ui/widgets/messagebytes.cc
Normal file
60
iqpilot/tools/cabana/ui/widgets/messagebytes.cc
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
ImVec2 byteCellSize() {
|
||||
pushMonoFont();
|
||||
|
||||
const ImFontBaked *baked = ImGui::GetFontBaked();
|
||||
const ImVec2 size(ImGui::CalcTextSize("00 ").x, std::ceil(baked->Ascent) - std::floor(baked->Descent) + 1 + 2);
|
||||
popMonoFont();
|
||||
return size;
|
||||
}
|
||||
|
||||
ImVec2 bytesCellSize(int n, bool multiple_lines) {
|
||||
const ImVec2 byte_size = byteCellSize();
|
||||
const int rows = multiple_lines ? std::max(1, (n + 7) / 8) : 1;
|
||||
const int columns = multiple_lines ? std::min(n, 8) : n;
|
||||
const ImVec2 margin = ImGui::GetStyle().CellPadding;
|
||||
return {columns * byte_size.x + (margin.x + 1) * 2, rows * byte_size.y + (margin.y + 1) * 2};
|
||||
}
|
||||
|
||||
ImU32 cellTextColor(bool selected, bool inactive) {
|
||||
if (selected) return inactive ? withAlpha(highlightedTextColor(), 100) : highlightedTextColor();
|
||||
return ImGui::GetColorU32(inactive ? ImGuiCol_TextDisabled : ImGuiCol_Text);
|
||||
}
|
||||
|
||||
void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive) {
|
||||
drawElidedText(dl, rect, text, cellTextColor(selected, inactive));
|
||||
}
|
||||
|
||||
void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector<uint8_t> &bytes, const std::vector<CabanaColor> *colors,
|
||||
bool selected, bool inactive, bool multiple_lines) {
|
||||
const ImU32 text_pen = cellTextColor(selected, inactive);
|
||||
const ImVec2 byte_size = byteCellSize();
|
||||
pushMonoFont();
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float font_size = ImGui::GetFontSize();
|
||||
for (int i = 0; i < (int)bytes.size(); ++i) {
|
||||
const int row = multiple_lines ? i / 8 : 0;
|
||||
const int column = multiple_lines ? i % 8 : i;
|
||||
const ImVec2 min(rect.Min.x + column * byte_size.x, rect.Min.y + row * byte_size.y);
|
||||
const ImRect r(min, ImVec2(min.x + byte_size.x, min.y + byte_size.y));
|
||||
|
||||
|
||||
ImU32 pen = text_pen;
|
||||
if (colors && i < (int)colors->size() && (*colors)[i].alpha() > 0) {
|
||||
if (selected) {
|
||||
pen = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
dl->AddRectFilled(r.Min, r.Max, ImGui::GetColorU32(ImGuiCol_WindowBg));
|
||||
}
|
||||
dl->AddRectFilled(r.Min, r.Max, toImU32((*colors)[i]));
|
||||
}
|
||||
drawText(dl, r, utils::hexByte(bytes[i]), pen, font, font_size);
|
||||
}
|
||||
popMonoFont();
|
||||
}
|
||||
20
iqpilot/tools/cabana/ui/widgets/messagebytes.h
Normal file
20
iqpilot/tools/cabana/ui/widgets/messagebytes.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/color.h"
|
||||
|
||||
|
||||
|
||||
|
||||
ImVec2 byteCellSize();
|
||||
ImVec2 bytesCellSize(int n, bool multiple_lines);
|
||||
ImU32 cellTextColor(bool selected, bool inactive);
|
||||
|
||||
void drawTextCell(ImDrawList *dl, const ImRect &rect, const std::string &text, bool selected, bool inactive);
|
||||
void drawBytesCell(ImDrawList *dl, const ImRect &rect, const std::vector<uint8_t> &bytes, const std::vector<CabanaColor> *colors,
|
||||
bool selected, bool inactive, bool multiple_lines);
|
||||
512
iqpilot/tools/cabana/ui/widgets/messageswidget.cc
Normal file
512
iqpilot/tools/cabana/ui/widgets/messageswidget.cc
Normal file
@@ -0,0 +1,512 @@
|
||||
#include "tools/cabana/ui/widgets/messageswidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cfloat>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <utility>
|
||||
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/widgets/messagebytes.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char *COLUMN_TITLES[MessageList::COLUMN_COUNT] = {"Name", "Bus", "ID", "Node", "Freq", "Count", "Bytes"};
|
||||
constexpr float DEFAULT_SECTION_SIZE = 100.0f;
|
||||
|
||||
|
||||
unsigned int toUInt(const std::string &s, bool *ok, int base) {
|
||||
const char *b = s.data(), *e = b + s.size();
|
||||
while (b < e && std::isspace((unsigned char)*b)) ++b;
|
||||
while (e > b && std::isspace((unsigned char)e[-1])) --e;
|
||||
unsigned int v = 0;
|
||||
auto [p, ec] = std::from_chars(b, e, v, base);
|
||||
*ok = b < e && p == e && ec == std::errc();
|
||||
return *ok ? v : 0;
|
||||
}
|
||||
|
||||
bool parseRange(const std::string &filter, uint32_t value, int base = 10) {
|
||||
|
||||
unsigned int min = std::numeric_limits<unsigned int>::min();
|
||||
unsigned int max = std::numeric_limits<unsigned int>::max();
|
||||
auto s = utils::split(filter, '-');
|
||||
bool ok = s.size() >= 1 && s.size() <= 2;
|
||||
if (ok && !s[0].empty()) min = toUInt(s[0], &ok, base);
|
||||
if (ok && s.size() == 1) {
|
||||
max = min;
|
||||
} else if (ok && s.size() == 2 && !s[1].empty()) {
|
||||
max = toUInt(s[1], &ok, base);
|
||||
}
|
||||
return ok && value >= min && value <= max;
|
||||
}
|
||||
|
||||
|
||||
|
||||
inline ImGuiSortDirection flipSortDirection(ImGuiSortDirection dir) {
|
||||
return dir == ImGuiSortDirection_Ascending ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending;
|
||||
}
|
||||
|
||||
std::string formatFreq(float freq) {
|
||||
if (freq <= 0) return "--";
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), freq >= 0.95 ? "%.0f" : "%.2f", freq >= 0.95 ? std::nearbyint(freq) : freq);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
std::string cellText(const MessageList::Item &item, int column) {
|
||||
const bool seen = item.id.source != INVALID_SOURCE;
|
||||
switch (column) {
|
||||
case MessageList::NAME: return item.name;
|
||||
case MessageList::SOURCE: return seen ? std::to_string(item.id.source) : "N/A";
|
||||
case MessageList::ADDRESS: return utils::toHexString(item.id.address);
|
||||
case MessageList::NODE: return item.node;
|
||||
case MessageList::FREQ: return seen ? formatFreq(can->lastMessage(item.id).freq) : "N/A";
|
||||
case MessageList::COUNT: return seen ? std::to_string(can->lastMessage(item.id).count) : "N/A";
|
||||
case MessageList::DATA: return seen ? "" : "N/A";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
MessageList::MessageList() {
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); }));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); }));
|
||||
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); }));
|
||||
}
|
||||
|
||||
void MessageList::setFilters(const std::map<int, std::string> &filters) {
|
||||
filters_ = filters;
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::showInactiveMessages(bool show) {
|
||||
show_inactive_messages = show;
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::dbcModified() {
|
||||
dbc_messages_.clear();
|
||||
for (const auto &[_, m] : dbc()->getMessages(-1)) {
|
||||
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
|
||||
}
|
||||
filterAndSort();
|
||||
}
|
||||
|
||||
void MessageList::sortItems(std::vector<Item> &list) {
|
||||
auto compare = [this](const auto &l, const auto &r) {
|
||||
switch (sort_column_) {
|
||||
case NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id);
|
||||
case SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address);
|
||||
case ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source);
|
||||
case NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id);
|
||||
case FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id);
|
||||
case COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id);
|
||||
default: return false;
|
||||
}
|
||||
};
|
||||
|
||||
if (sort_order_ == ImGuiSortDirection_Descending)
|
||||
std::stable_sort(list.rbegin(), list.rend(), compare);
|
||||
else
|
||||
std::stable_sort(list.begin(), list.end(), compare);
|
||||
}
|
||||
|
||||
bool MessageList::match(const Item &item) {
|
||||
if (filters_.empty()) return true;
|
||||
|
||||
bool match = true;
|
||||
const auto &data = can->lastMessage(item.id);
|
||||
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
||||
const std::string &txt = it->second;
|
||||
switch (it->first) {
|
||||
case NAME: {
|
||||
match = utils::containsCI(item.name, txt);
|
||||
if (!match) {
|
||||
const auto m = dbc()->msg(item.id);
|
||||
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
|
||||
[&txt](const auto &s) { return utils::containsCI(s->name, txt); });
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SOURCE:
|
||||
match = parseRange(txt, item.id.source);
|
||||
break;
|
||||
case ADDRESS:
|
||||
match = utils::containsCI(utils::toHexString(item.id.address), txt);
|
||||
match = match || parseRange(txt, item.id.address, 16);
|
||||
break;
|
||||
case NODE:
|
||||
match = utils::containsCI(item.node, txt);
|
||||
break;
|
||||
case FREQ:
|
||||
match = parseRange(txt, data.freq);
|
||||
break;
|
||||
case COUNT:
|
||||
match = parseRange(txt, data.count);
|
||||
break;
|
||||
case DATA:
|
||||
match = utils::containsCI(utils::toHex(data.dat), txt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
bool MessageList::filterAndSort() {
|
||||
|
||||
std::vector<MessageId> all_messages;
|
||||
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
|
||||
auto dbc_msgs = dbc_messages_;
|
||||
for (const auto &[id, m] : can->lastMessages()) {
|
||||
all_messages.push_back(id);
|
||||
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
|
||||
}
|
||||
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
|
||||
|
||||
std::vector<Item> new_items;
|
||||
new_items.reserve(all_messages.size());
|
||||
for (const auto &id : all_messages) {
|
||||
if (show_inactive_messages || can->isMessageActive(id)) {
|
||||
auto msg = dbc()->msg(id);
|
||||
Item item = {.id = id, .name = msg ? msg->name : UNTITLED, .node = msg ? msg->transmitter : std::string()};
|
||||
if (match(item)) new_items.emplace_back(item);
|
||||
}
|
||||
}
|
||||
sortItems(new_items);
|
||||
|
||||
if (items != new_items) {
|
||||
items = std::move(new_items);
|
||||
changed();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void MessageList::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
|
||||
if (has_new_ids || ((filters_.count(FREQ) || filters_.count(COUNT) || filters_.count(DATA)) &&
|
||||
++sort_threshold_ == STREAM_UPDATE_FPS)) {
|
||||
sort_threshold_ = 0;
|
||||
filterAndSort();
|
||||
}
|
||||
}
|
||||
|
||||
void MessageList::sort(int column, ImGuiSortDirection order) {
|
||||
if (column != DATA) {
|
||||
sort_column_ = column;
|
||||
sort_order_ = order;
|
||||
filterAndSort();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
MessagesWidget::MessagesWidget() {
|
||||
std::iota(display_order_.begin(), display_order_.end(), 0);
|
||||
list_.sort(MessageList::NAME, ImGuiSortDirection_Ascending);
|
||||
|
||||
connections_.push_back(list_.changed.connect([this]() {
|
||||
current_row_ = -1;
|
||||
if (current_msg_id_) selectMessage(*current_msg_id_);
|
||||
updateBytesSectionSize();
|
||||
updateTitle();
|
||||
}));
|
||||
|
||||
suppressHighlighted();
|
||||
}
|
||||
|
||||
std::string MessagesWidget::whatsThis() const {
|
||||
return R"(
|
||||
<b>Message View</b><br/>
|
||||
<span style="color:gray">Byte color</span><br />
|
||||
<span style="color:gray;">■ </span> constant changing<br />
|
||||
<span style="color:blue;">■ </span> increasing<br />
|
||||
<span style="color:red;">■ </span> decreasing<br />
|
||||
<span style="color:gray">Shortcuts</span><br />
|
||||
Horizontal Scrolling: <span style="background-color:lightGray;color:gray"> shift+wheel </span>
|
||||
)";
|
||||
}
|
||||
|
||||
void MessagesWidget::drawToolBar() {
|
||||
ImGui::Dummy(ImVec2(0, std::max(0.0f, 9 - ImGui::GetStyle().ItemSpacing.y)));
|
||||
if (ImGui::Button("Suppress Highlighted")) suppressHighlighted(true);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!suppress_clear_enabled_);
|
||||
const std::string clear_label = suppress_clear_text_ + "##suppress_clear";
|
||||
if (ImGui::Button(clear_label.c_str())) suppressHighlighted(false);
|
||||
ImGui::EndDisabled();
|
||||
disabledItemTooltip("Clear suppressed");
|
||||
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float checkbox_width = ImGui::CalcTextSize("Suppress Signals").x + ImGui::GetFrameHeight() + style.ItemInnerSpacing.x;
|
||||
const float view_button_width = ImGui::CalcTextSize(icon::THREE_DOTS).x + style.FramePadding.x * 2;
|
||||
alignRight(checkbox_width + style.ItemSpacing.x + view_button_width);
|
||||
|
||||
bool suppress_defined_signals = settings.suppress_defined_signals;
|
||||
if (checkBox("Suppress Signals", &suppress_defined_signals)) can->suppressDefinedSignals(suppress_defined_signals);
|
||||
ImGui::SetItemTooltip("Suppress defined signals");
|
||||
ImGui::SameLine();
|
||||
|
||||
if (toolButton("view_btn", icon::THREE_DOTS, "View...")) ImGui::OpenPopup("menu");
|
||||
}
|
||||
|
||||
void MessagesWidget::updateTitle() {
|
||||
auto stats = std::accumulate(
|
||||
list_.items.begin(), list_.items.end(), std::pair<size_t, size_t>(),
|
||||
[](const auto &pair, const auto &item) {
|
||||
auto m = dbc()->msg(item.id);
|
||||
return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair;
|
||||
});
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%zu Messages (%zu DBC Messages, %zu Signals)", list_.items.size(), stats.first, stats.second);
|
||||
title_ = buf;
|
||||
}
|
||||
|
||||
void MessagesWidget::selectMessage(const MessageId &msg_id) {
|
||||
auto it = std::find_if(list_.items.cbegin(), list_.items.cend(), [&msg_id](auto &item) { return item.id == msg_id; });
|
||||
if (it != list_.items.cend()) setCurrentRow(std::distance(list_.items.cbegin(), it));
|
||||
}
|
||||
|
||||
void MessagesWidget::setCurrentRow(int row) {
|
||||
if (row < 0 || row >= (int)list_.items.size()) return;
|
||||
current_row_ = row;
|
||||
scroll_to_current_ = true;
|
||||
const auto &id = list_.items[row].id;
|
||||
if (!current_msg_id_ || id != *current_msg_id_) {
|
||||
current_msg_id_ = id;
|
||||
msgSelectionChanged(*current_msg_id_);
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::suppressHighlighted(bool from_suppress_add) {
|
||||
int n = from_suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0);
|
||||
suppress_clear_text_ = n > 0 ? "Clear (" + std::to_string(n) + ")" : "Clear";
|
||||
suppress_clear_enabled_ = n > 0;
|
||||
}
|
||||
|
||||
void MessagesWidget::drawContextMenu() {
|
||||
if (!ImGui::BeginPopup("menu")) return;
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
|
||||
const int column = display_order_[i];
|
||||
|
||||
if (ImGui::MenuItem(COLUMN_TITLES[column], nullptr, !hidden_[column], column > 0)) {
|
||||
pending_hidden_.emplace_back(column, !hidden_[column]);
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Multi-Line bytes", nullptr, settings.multiple_lines_hex)) {
|
||||
setMultiLineBytes(!settings.multiple_lines_hex);
|
||||
}
|
||||
if (ImGui::MenuItem("Show inactive messages", nullptr, list_.show_inactive_messages)) {
|
||||
list_.showInactiveMessages(!list_.show_inactive_messages);
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void MessagesWidget::setMultiLineBytes(bool multi) {
|
||||
settings.multiple_lines_hex = multi;
|
||||
updateBytesSectionSize();
|
||||
}
|
||||
|
||||
void MessagesWidget::updateBytesSectionSize() {
|
||||
int max_bytes = 8;
|
||||
if (!settings.multiple_lines_hex) {
|
||||
for (const auto &[_, m] : can->lastMessages()) {
|
||||
max_bytes = std::max<int>(max_bytes, m.dat.size());
|
||||
}
|
||||
}
|
||||
bytes_section_bytes_ = max_bytes;
|
||||
}
|
||||
|
||||
void MessagesWidget::draw() {
|
||||
drawToolBar();
|
||||
drawTable();
|
||||
if (std::exchange(header_menu_requested_, false)) ImGui::OpenPopup("menu");
|
||||
drawContextMenu();
|
||||
}
|
||||
|
||||
void MessagesWidget::handleKeys() {
|
||||
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || ImGui::IsAnyItemActive()) return;
|
||||
const int last = (int)list_.items.size() - 1;
|
||||
if (last < 0) return;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && current_row_ > 0) {
|
||||
setCurrentRow(current_row_ - 1);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && current_row_ < last) {
|
||||
setCurrentRow(current_row_ + 1);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
|
||||
setCurrentRow(0);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_End)) {
|
||||
setCurrentRow(last);
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
|
||||
setCurrentRow(std::max(current_row_ - visible_rows_, 0));
|
||||
} else if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
|
||||
setCurrentRow(std::min(current_row_ + visible_rows_, last));
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::drawTable() {
|
||||
handleKeys();
|
||||
const bool multiple_lines = settings.multiple_lines_hex;
|
||||
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
|
||||
ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders |
|
||||
ImGuiTableFlags_Hideable;
|
||||
|
||||
const float bytes_width = bytesCellSize(bytes_section_bytes_, multiple_lines).x;
|
||||
const float avail_width = ImGui::GetContentRegionAvail().x - (has_scrollbar_y_ ? ImGui::GetStyle().ScrollbarSize : 0);
|
||||
const float inner_width = std::max(avail_width, fixed_columns_width_ + bytes_width);
|
||||
if (!ImGui::BeginTable("messages", MessageList::COLUMN_COUNT, flags, ImVec2(0, 0), inner_width)) return;
|
||||
|
||||
|
||||
ImGui::TableSetupScrollFreeze(0, 2);
|
||||
|
||||
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
table->DisableDefaultContextMenu = true;
|
||||
table->IsContextPopupOpen = false;
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
|
||||
|
||||
ImGuiTableColumnFlags column_flags = ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_PreferSortDescending;
|
||||
float width = DEFAULT_SECTION_SIZE;
|
||||
if (i == MessageList::NAME) {
|
||||
column_flags |= ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoHide;
|
||||
} else if (i == MessageList::DATA) {
|
||||
column_flags = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_NoResize;
|
||||
width = 0;
|
||||
}
|
||||
ImGui::TableSetupColumn(COLUMN_TITLES[i], column_flags, width);
|
||||
}
|
||||
for (const auto &[column, hide] : pending_hidden_) ImGui::TableSetColumnEnabled(column, !hide);
|
||||
pending_hidden_.clear();
|
||||
|
||||
if (ImGuiTableSortSpecs *specs = ImGui::TableGetSortSpecs(); specs && specs->SpecsDirty) {
|
||||
if (specs->SpecsCount > 0) list_.sort(specs->Specs[0].ColumnIndex, flipSortDirection(specs->Specs[0].SortDirection));
|
||||
specs->SpecsDirty = false;
|
||||
if (current_row_ >= 0) scroll_to_current_ = true;
|
||||
}
|
||||
|
||||
drawHeader();
|
||||
|
||||
const int rows = list_.items.size();
|
||||
if (!multiple_lines) {
|
||||
ImGuiListClipper clipper;
|
||||
clipper.Begin(rows);
|
||||
if (scroll_to_current_ && current_row_ >= 0) clipper.IncludeItemByIndex(current_row_);
|
||||
while (clipper.Step()) {
|
||||
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) drawRow(row);
|
||||
}
|
||||
} else {
|
||||
|
||||
for (int row = 0; row < rows; ++row) drawRow(row);
|
||||
}
|
||||
|
||||
|
||||
const ImGuiTableColumn &data_column = table->Columns[MessageList::DATA];
|
||||
fixed_columns_width_ = data_column.IsEnabled ? table->ColumnsGivenWidth - data_column.WidthGiven : 0;
|
||||
has_scrollbar_y_ = table->InnerWindow->ScrollbarY;
|
||||
visible_rows_ = std::max(1, (int)(table->InnerWindow->InnerRect.GetHeight() / bytesCellSize(0, multiple_lines).y) - 2);
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void MessagesWidget::drawHeader() {
|
||||
if (tableHeadersRow() >= 0) header_menu_requested_ = true;
|
||||
|
||||
ImGuiTable *table = ImGui::GetCurrentTable();
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
|
||||
display_order_[i] = table->DisplayOrderToIndex[i];
|
||||
hidden_[i] = !(ImGui::TableGetColumnFlags(i) & ImGuiTableColumnFlags_IsEnabled);
|
||||
}
|
||||
|
||||
|
||||
const float clear_width = ImGui::CalcTextSize(icon::X).x + ImGui::GetStyle().FramePadding.x * 2;
|
||||
ImGui::TableNextRow();
|
||||
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
|
||||
if (!ImGui::TableSetColumnIndex(i)) continue;
|
||||
ImGui::PushID(i);
|
||||
ImGui::SetNextItemWidth(filters_[i].empty() ? -FLT_MIN : std::max(1.0f, ImGui::GetContentRegionAvail().x - clear_width));
|
||||
const std::string placeholder = std::string("Filter ") + COLUMN_TITLES[i];
|
||||
if (clearableInput("##filter", &filters_[i], placeholder.c_str())) {
|
||||
std::map<int, std::string> filters;
|
||||
for (int c = 0; c < MessageList::COLUMN_COUNT; ++c) {
|
||||
if (!filters_[c].empty()) filters[c] = filters_[c];
|
||||
}
|
||||
list_.setFilters(filters);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
void MessagesWidget::drawRow(int row) {
|
||||
const auto &item = list_.items[row];
|
||||
const bool selected = row == current_row_;
|
||||
const bool inactive = !can->isMessageActive(item.id);
|
||||
const auto &m = can->lastMessage(item.id);
|
||||
const bool seen = item.id.source != INVALID_SOURCE;
|
||||
const bool multiple_lines = settings.multiple_lines_hex;
|
||||
const float row_height = bytesCellSize(seen ? m.dat.size() : 0, multiple_lines).y - ImGui::GetStyle().CellPadding.y * 2;
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(row);
|
||||
|
||||
bool row_item_submitted = false;
|
||||
for (int column = 0; column < MessageList::COLUMN_COUNT; ++column) {
|
||||
if (!ImGui::TableSetColumnIndex(column)) continue;
|
||||
const ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
const float width = ImGui::GetContentRegionAvail().x;
|
||||
const ImRect rect(pos, ImVec2(pos.x + width, pos.y + row_height));
|
||||
|
||||
const bool row_item = !row_item_submitted;
|
||||
if (row_item) {
|
||||
|
||||
|
||||
row_item_submitted = true;
|
||||
|
||||
if (viewSelectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnClick, ImVec2(0, row_height))) {
|
||||
setCurrentRow(row);
|
||||
}
|
||||
if (selected && scroll_to_current_) {
|
||||
|
||||
const ImGuiWindow *inner = ImGui::GetCurrentTable()->InnerWindow;
|
||||
const float view_top = inner->InnerClipRect.Min.y + inner->DecoInnerSizeY1;
|
||||
const float view_bottom = inner->InnerClipRect.Max.y;
|
||||
if (ImGui::GetItemRectMin().y < view_top) {
|
||||
ImGui::SetScrollHereY(0.0f);
|
||||
} else if (ImGui::GetItemRectMax().y > view_bottom) {
|
||||
ImGui::SetScrollHereY(1.0f);
|
||||
}
|
||||
scroll_to_current_ = false;
|
||||
}
|
||||
|
||||
const ImGuiTableColumn &name_col = ImGui::GetCurrentTable()->Columns[MessageList::NAME];
|
||||
const float mouse_x = ImGui::GetIO().MousePos.x;
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && mouse_x >= name_col.MinX && mouse_x < name_col.MaxX) {
|
||||
auto msg = dbc()->msg(item.id);
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::TextUnformatted(item.name.c_str());
|
||||
if (msg && !msg->comment.empty()) ImGui::TextDisabled("%s", msg->comment.c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
if (column == MessageList::DATA && seen) {
|
||||
drawBytesCell(ImGui::GetWindowDrawList(), rect, m.dat, &m.colors, selected, inactive, multiple_lines);
|
||||
} else {
|
||||
drawTextCell(ImGui::GetWindowDrawList(), rect, cellText(item, column), selected, inactive);
|
||||
}
|
||||
|
||||
if (!row_item) ImGui::Dummy(ImVec2(width, row_height));
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
93
iqpilot/tools/cabana/ui/widgets/messageswidget.h
Normal file
93
iqpilot/tools/cabana/ui/widgets/messageswidget.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
|
||||
|
||||
class MessageList {
|
||||
public:
|
||||
enum Column { NAME = 0, SOURCE, ADDRESS, NODE, FREQ, COUNT, DATA, COLUMN_COUNT };
|
||||
|
||||
struct Item {
|
||||
MessageId id;
|
||||
std::string name;
|
||||
std::string node;
|
||||
bool operator==(const Item &other) const { return id == other.id && name == other.name && node == other.node; }
|
||||
};
|
||||
|
||||
MessageList();
|
||||
void sort(int column, ImGuiSortDirection order);
|
||||
void setFilters(const std::map<int, std::string> &filters);
|
||||
void showInactiveMessages(bool show);
|
||||
bool filterAndSort();
|
||||
|
||||
std::vector<Item> items;
|
||||
bool show_inactive_messages = true;
|
||||
Observable<> changed;
|
||||
|
||||
private:
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
void dbcModified();
|
||||
void sortItems(std::vector<Item> &list);
|
||||
bool match(const Item &item);
|
||||
|
||||
std::map<int, std::string> filters_;
|
||||
std::set<MessageId> dbc_messages_;
|
||||
int sort_column_ = NAME;
|
||||
ImGuiSortDirection sort_order_ = ImGuiSortDirection_Ascending;
|
||||
int sort_threshold_ = 0;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
class MessagesWidget {
|
||||
public:
|
||||
MessagesWidget();
|
||||
void draw();
|
||||
void selectMessage(const MessageId &message_id);
|
||||
void suppressHighlighted(bool from_suppress_add = false);
|
||||
const std::string &title() const { return title_; }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const MessageId &> msgSelectionChanged;
|
||||
|
||||
private:
|
||||
void drawToolBar();
|
||||
void drawTable();
|
||||
void drawHeader();
|
||||
void drawRow(int row);
|
||||
void drawContextMenu();
|
||||
void handleKeys();
|
||||
void setCurrentRow(int row);
|
||||
void updateBytesSectionSize();
|
||||
void updateTitle();
|
||||
void setMultiLineBytes(bool multi);
|
||||
|
||||
MessageList list_;
|
||||
std::optional<MessageId> current_msg_id_;
|
||||
int current_row_ = -1;
|
||||
bool scroll_to_current_ = false;
|
||||
int bytes_section_bytes_ = 8;
|
||||
float fixed_columns_width_ = 0;
|
||||
bool has_scrollbar_y_ = false;
|
||||
int visible_rows_ = 1;
|
||||
std::array<std::string, MessageList::COLUMN_COUNT> filters_;
|
||||
std::array<bool, MessageList::COLUMN_COUNT> hidden_ = {};
|
||||
std::array<int, MessageList::COLUMN_COUNT> display_order_;
|
||||
std::vector<std::pair<int, bool>> pending_hidden_;
|
||||
bool header_menu_requested_ = false;
|
||||
std::string suppress_clear_text_;
|
||||
bool suppress_clear_enabled_ = false;
|
||||
std::string title_ = "MESSAGES";
|
||||
Connections connections_;
|
||||
};
|
||||
87
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.cc
Normal file
87
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.cc
Normal file
@@ -0,0 +1,87 @@
|
||||
#include "tools/cabana/ui/widgets/scrollabletabbar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui_internal.h"
|
||||
|
||||
namespace {
|
||||
float scrollButtonsWidth() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
return ImGui::GetFrameHeight() * 2.0f + style.ItemInnerSpacing.x + style.ItemSpacing.x * 2.0f;
|
||||
}
|
||||
|
||||
void drawScrollButtons(ImGuiTabBar *tab_bar) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float size = ImGui::GetFrameHeight();
|
||||
const float max_scroll = std::max(0.0f, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
|
||||
const float start_x = tab_bar->BarRect.Max.x + style.ItemSpacing.x;
|
||||
const ImVec2 backup_pos = ImGui::GetCursorScreenPos();
|
||||
|
||||
ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const bool left = i == 0;
|
||||
ImGui::SetCursorScreenPos(ImVec2(start_x + i * (size + style.ItemInnerSpacing.x), tab_bar->BarRect.Min.y));
|
||||
ImGui::BeginDisabled(left ? tab_bar->ScrollingTarget <= 0.0f : tab_bar->ScrollingTarget >= max_scroll);
|
||||
if (ImGui::Button(left ? "###scroll_left" : "###scroll_right", ImVec2(size, size))) {
|
||||
const float step = (left ? -4.0f : 4.0f) * ImGui::GetFontSize();
|
||||
tab_bar->ScrollingTarget = std::clamp(tab_bar->ScrollingTarget + step, 0.0f, max_scroll);
|
||||
tab_bar->ScrollingAnim = tab_bar->ScrollingTarget;
|
||||
}
|
||||
|
||||
const ImVec2 c((ImGui::GetItemRectMin().x + ImGui::GetItemRectMax().x) * 0.5f,
|
||||
(ImGui::GetItemRectMin().y + ImGui::GetItemRectMax().y) * 0.5f);
|
||||
const float h = std::round(ImGui::GetFontSize() * 0.25f);
|
||||
const float dx = left ? h * 0.5f : -h * 0.5f;
|
||||
ImDrawList *painter = ImGui::GetWindowDrawList();
|
||||
painter->PathLineTo(ImVec2(c.x + dx, c.y - h));
|
||||
painter->PathLineTo(ImVec2(c.x - dx, c.y));
|
||||
painter->PathLineTo(ImVec2(c.x + dx, c.y + h));
|
||||
painter->PathStroke(ImGui::GetColorU32(ImGuiCol_Text), ImDrawFlags_None, 1.5f);
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::PopItemFlag();
|
||||
ImGui::SetCursorScreenPos(backup_pos);
|
||||
}
|
||||
|
||||
struct ScrollableTabBar { ImGuiTabBar *tab_bar; bool overflowing; };
|
||||
std::vector<ScrollableTabBar> scrollable_tab_bars;
|
||||
}
|
||||
|
||||
bool beginScrollableTabBar(const char *str_id, ImGuiTabBarFlags flags) {
|
||||
|
||||
ImGuiWindow *window = ImGui::GetCurrentWindow();
|
||||
ImGuiTabBar *prev_tab_bar = ImGui::TabBarFindByID(window->GetID(str_id));
|
||||
const bool overflowing = prev_tab_bar && prev_tab_bar->WidthAllTabsIdeal > prev_tab_bar->BarRect.GetWidth() + 1.0f;
|
||||
const float backup_work_max_x = window->WorkRect.Max.x;
|
||||
if (overflowing) window->WorkRect.Max.x -= scrollButtonsWidth();
|
||||
const bool open = ImGui::BeginTabBar(str_id, flags | ImGuiTabBarFlags_FittingPolicyScroll | ImGuiTabBarFlags_NoTabListScrollingButtons);
|
||||
window->WorkRect.Max.x = backup_work_max_x;
|
||||
if (open) scrollable_tab_bars.push_back({ImGui::GetCurrentTabBar(), overflowing});
|
||||
return open;
|
||||
}
|
||||
|
||||
void endScrollableTabBar() {
|
||||
ImGui::EndTabBar();
|
||||
const ScrollableTabBar bar = scrollable_tab_bars.back();
|
||||
scrollable_tab_bars.pop_back();
|
||||
if (!bar.overflowing) return;
|
||||
drawScrollButtons(bar.tab_bar);
|
||||
|
||||
|
||||
|
||||
ImGuiTabBar *tab_bar = bar.tab_bar;
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max)) {
|
||||
ImGui::SetKeyOwner(ImGuiKey_MouseWheelX, tab_bar->ID);
|
||||
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, tab_bar->ID);
|
||||
const ImGuiIO &io = ImGui::GetIO();
|
||||
const float wheel = io.MouseWheelH + io.MouseWheel;
|
||||
if (wheel != 0.0f) {
|
||||
const float max_scroll = std::max(0.0f, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth());
|
||||
const float step = std::floor(ImGui::GetFontSize() * 2.0f);
|
||||
tab_bar->ScrollingTarget = std::clamp(tab_bar->ScrollingTarget - wheel * step, 0.0f, max_scroll);
|
||||
tab_bar->ScrollingAnim = tab_bar->ScrollingTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.h
Normal file
8
iqpilot/tools/cabana/ui/widgets/scrollabletabbar.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
|
||||
|
||||
bool beginScrollableTabBar(const char *str_id, ImGuiTabBarFlags flags = 0);
|
||||
void endScrollableTabBar();
|
||||
975
iqpilot/tools/cabana/ui/widgets/signalview.cc
Normal file
975
iqpilot/tools/cabana/ui/widgets/signalview.cc
Normal file
@@ -0,0 +1,975 @@
|
||||
#include "tools/cabana/ui/widgets/signalview.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <future>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/dialogs/messagebox.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
|
||||
namespace {
|
||||
constexpr float INDENTATION = 20.0f;
|
||||
constexpr float H_MARGIN = 3.0f;
|
||||
constexpr float V_MARGIN = 2.0f;
|
||||
|
||||
constexpr float SIGNAL_ROW_EXTRA = 5.0f;
|
||||
constexpr float SIGNAL_ROW_SCALE = 1.25f;
|
||||
constexpr float FILTER_WIDTH = 160.0f;
|
||||
constexpr float SPARKLINE_SLIDER_WIDTH = 120.0f;
|
||||
constexpr float COLLAPSE_ICON_SIZE = 12.0f;
|
||||
|
||||
|
||||
constexpr int SPARKLINE_RANGE_MAX = 30;
|
||||
constexpr float LABEL_FONT = 12.0f;
|
||||
constexpr float MINMAX_FONT = 10.0f;
|
||||
constexpr int COLOR_LABEL_WIDTH = 18;
|
||||
|
||||
std::string signalTypeToString(cabana::Signal::Type type) {
|
||||
if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal";
|
||||
else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal";
|
||||
else return "Normal Signal";
|
||||
}
|
||||
|
||||
std::string multiplexIndicator(const cabana::Signal *sig) {
|
||||
return sig->type == cabana::Signal::Type::Multiplexor ? std::string(" M ") : " m" + std::to_string(sig->multiplex_value) + " ";
|
||||
}
|
||||
|
||||
std::string nameText(const SignalModel::Item *item) {
|
||||
return item->type == SignalModel::Item::Sig ? item->sig->name : item->title;
|
||||
}
|
||||
|
||||
float rowHeight() {
|
||||
return ImGui::GetFrameHeight();
|
||||
}
|
||||
|
||||
|
||||
float signalRowHeight() {
|
||||
return std::floor((ImGui::GetFrameHeight() + SIGNAL_ROW_EXTRA) * SIGNAL_ROW_SCALE);
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool valueDescriptionEditor(int column, std::string *text) {
|
||||
ImGui::PushID(column);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
|
||||
validatedInput("##edit", text, column == 0 ? doubleValidator : nullptr);
|
||||
ImGui::PopStyleVar();
|
||||
const bool clicked = ImGui::IsItemActivated() || ImGui::IsItemClicked();
|
||||
ImGui::PopID();
|
||||
return clicked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SignalModel::SignalModel() : root_(new Item) {
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
|
||||
connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); }));
|
||||
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); }));
|
||||
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); }));
|
||||
}
|
||||
|
||||
void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) {
|
||||
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name};
|
||||
root_item->children.insert(root_item->children.begin() + pos, parent_item);
|
||||
std::string titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
|
||||
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
|
||||
for (int i = 0; i < std::size(titles); ++i) {
|
||||
auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]};
|
||||
parent_item->children.push_back(item);
|
||||
if (item->type == Item::ExtraInfo) {
|
||||
parent_item = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::setMessage(const MessageId &id) {
|
||||
msg_id_ = id;
|
||||
filter_str_ = "";
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SignalModel::setFilter(const std::string &txt) {
|
||||
filter_str_ = txt;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SignalModel::refresh() {
|
||||
root_.reset(new SignalModel::Item);
|
||||
if (auto msg = dbc()->msg(msg_id_)) {
|
||||
for (auto s : msg->getSignals()) {
|
||||
if (filter_str_.empty() || utils::containsCI(s->name, filter_str_)) {
|
||||
insertItem(root_.get(), root_->children.size(), s);
|
||||
}
|
||||
}
|
||||
}
|
||||
modelReset();
|
||||
rowsChanged();
|
||||
}
|
||||
|
||||
bool SignalModel::isEnabled(const Item *item) {
|
||||
return !(item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed);
|
||||
}
|
||||
|
||||
bool SignalModel::isCheckable(const Item *item) {
|
||||
return item->type == Item::Endian || item->type == Item::Signed;
|
||||
}
|
||||
|
||||
bool SignalModel::isEditable(const Item *item) {
|
||||
return item->children.empty() && !isCheckable(item);
|
||||
}
|
||||
|
||||
int SignalModel::signalRow(const cabana::Signal *sig) const {
|
||||
for (int i = 0; i < root_->children.size(); ++i) {
|
||||
if (root_->children[i]->sig == sig) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string SignalModel::valueText(const Item *item) const {
|
||||
switch (item->type) {
|
||||
case Item::Sig: return item->sig_val;
|
||||
case Item::Name: return item->sig->name;
|
||||
case Item::Size: return std::to_string(item->sig->size);
|
||||
case Item::Node: return item->sig->receiver_name;
|
||||
case Item::SignalType: return signalTypeToString(item->sig->type);
|
||||
case Item::MultiplexValue: return std::to_string(item->sig->multiplex_value);
|
||||
case Item::Offset: return doubleToString(item->sig->offset);
|
||||
case Item::Factor: return doubleToString(item->sig->factor);
|
||||
case Item::Unit: return item->sig->unit;
|
||||
case Item::Comment: return item->sig->comment;
|
||||
case Item::Min: return doubleToString(item->sig->min);
|
||||
case Item::Max: return doubleToString(item->sig->max);
|
||||
case Item::Desc: {
|
||||
std::string val_desc;
|
||||
for (auto &[val, desc] : item->sig->val_desc) {
|
||||
if (!val_desc.empty()) val_desc += " ";
|
||||
val_desc += utils::toString(val) + " \"" + desc + "\"";
|
||||
}
|
||||
return val_desc;
|
||||
}
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
bool SignalModel::setData(Item *item, const ItemValue &value) {
|
||||
cabana::Signal s = *item->sig;
|
||||
switch (item->type) {
|
||||
case Item::Name: s.name = value.toString(); break;
|
||||
case Item::Size: s.size = value.toInt(); break;
|
||||
case Item::Node: s.receiver_name = utils::trimmed(value.toString()); break;
|
||||
case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break;
|
||||
case Item::MultiplexValue: s.multiplex_value = value.toInt(); break;
|
||||
case Item::Endian: s.is_little_endian = value.toBool(); break;
|
||||
case Item::Signed: s.is_signed = value.toBool(); break;
|
||||
case Item::Offset: s.offset = value.toDouble(); break;
|
||||
case Item::Factor: s.factor = value.toDouble(); break;
|
||||
case Item::Unit: s.unit = value.toString(); break;
|
||||
case Item::Comment: s.comment = value.toString(); break;
|
||||
case Item::Min: s.min = value.toDouble(); break;
|
||||
case Item::Max: s.max = value.toDouble(); break;
|
||||
case Item::Desc: s.val_desc = value.toValueDescription(); break;
|
||||
default: return false;
|
||||
}
|
||||
return saveSignal(item->sig, s);
|
||||
}
|
||||
|
||||
bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) {
|
||||
auto msg = dbc()->msg(msg_id_);
|
||||
if (s.name != origin_s->name && msg->sig(s.name) != nullptr) {
|
||||
std::string text = "There is already a signal with the same name '" + s.name + "'";
|
||||
MessageBox::warning("Failed to save signal", text);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s.is_little_endian != origin_s->is_little_endian) {
|
||||
s.start_bit = flipBitPos(s.start_bit);
|
||||
}
|
||||
UndoStack::instance()->push(new EditSignalCommand(msg_id_, origin_s, s));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalModel::handleMsgChanged(MessageId id) {
|
||||
if (id.address == msg_id_.address) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
||||
if (id == msg_id_) {
|
||||
if (filter_str_.empty()) {
|
||||
int i = dbc()->msg(msg_id_)->indexOf(sig);
|
||||
insertItem(root_.get(), i, sig);
|
||||
rowsChanged();
|
||||
} else if (utils::containsCI(sig->name, filter_str_)) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
|
||||
if (int row = signalRow(sig); row != -1) {
|
||||
if (filter_str_.empty()) {
|
||||
|
||||
int to = dbc()->msg(msg_id_)->indexOf(sig);
|
||||
if (to != row) {
|
||||
auto item = root_->children[row];
|
||||
root_->children.erase(root_->children.begin() + row);
|
||||
root_->children.insert(root_->children.begin() + to, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
|
||||
if (int row = signalRow(sig); row != -1) {
|
||||
delete root_->children[row];
|
||||
root_->children.erase(root_->children.begin() + row);
|
||||
rowsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
float SignalView::textWidth(const std::string &text, float font_size) {
|
||||
ImFont *font = ImGui::GetFont();
|
||||
if (!font || ImGui::GetFontSize() <= 0) return 0;
|
||||
return font->CalcTextSizeA(font_size > 0 ? font_size : ImGui::GetFontSize(), FLT_MAX, 0.0f, text.c_str()).x;
|
||||
}
|
||||
|
||||
float SignalView::nameColumnWidth(const SignalModel::Item *item, float widget_width, const std::string &text) const {
|
||||
float spacing = INDENTATION + COLOR_LABEL_WIDTH + 8;
|
||||
std::string txt = text;
|
||||
if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) {
|
||||
txt += multiplexIndicator(item->sig);
|
||||
spacing += H_MARGIN * 2;
|
||||
}
|
||||
return std::min<float>(widget_width / 3.0, textWidth(txt) + spacing);
|
||||
}
|
||||
|
||||
void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const SignalModel::Item *item, int column,
|
||||
bool selected, const std::string &text, float viewport_x) const {
|
||||
const float h_margin = H_MARGIN;
|
||||
const float v_margin = V_MARGIN;
|
||||
|
||||
ImRect rect(option_rect.Min.x + h_margin, option_rect.Min.y + v_margin, option_rect.Max.x - h_margin, option_rect.Max.y - v_margin);
|
||||
|
||||
const ImU32 text_color = selected ? highlightedTextColor() : ImGui::GetColorU32(ImGuiCol_Text);
|
||||
|
||||
if (column == 0) {
|
||||
if (item->type == SignalModel::Item::Sig) {
|
||||
|
||||
ImRect icon_rect(rect.Min.x, rect.Min.y, rect.Min.x + COLOR_LABEL_WIDTH, rect.Max.y);
|
||||
painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(item->sig->color.darker(item->highlight ? 125 : 0)), 3.0f);
|
||||
drawText(painter, icon_rect, std::to_string(item->row() + 1).c_str(), item->highlight ? IM_COL32_WHITE : IM_COL32_BLACK,
|
||||
nullptr, LABEL_FONT);
|
||||
|
||||
rect.Min.x = icon_rect.Max.x + h_margin * 2;
|
||||
|
||||
if (item->sig->type != cabana::Signal::Type::Normal) {
|
||||
const std::string indicator = multiplexIndicator(item->sig);
|
||||
ImRect indicator_rect(rect.Min.x, rect.Min.y, rect.Min.x + ImGui::CalcTextSize(indicator.c_str()).x, rect.Max.y);
|
||||
painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), 3.0f);
|
||||
drawElidedText(painter, indicator_rect, indicator, IM_COL32_WHITE, false);
|
||||
rect.Min.x = indicator_rect.Max.x + h_margin * 2;
|
||||
}
|
||||
} else {
|
||||
rect.Min.x = viewport_x + INDENTATION + COLOR_LABEL_WIDTH + h_margin * 3;
|
||||
}
|
||||
|
||||
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, false);
|
||||
} else if (column == 1) {
|
||||
if (!item->sparkline.isEmpty()) {
|
||||
const ImVec2 sparkline_size = item->sparkline.size;
|
||||
item->sparkline.draw(painter, rect.Min);
|
||||
|
||||
rect.Min.x += sparkline_size.x + 1;
|
||||
float value_adjust = 10;
|
||||
if (item->highlight || selected) {
|
||||
painter->AddLine(rect.Min, ImVec2(rect.Min.x, rect.Max.y), text_color);
|
||||
rect.Min.x += 5;
|
||||
rect.Min.y -= v_margin;
|
||||
rect.Max.y += v_margin;
|
||||
std::string min = utils::toString(item->sparkline.min_val);
|
||||
std::string max = utils::toString(item->sparkline.max_val);
|
||||
drawText(painter, rect, max.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 0.0f));
|
||||
drawText(painter, rect, min.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 1.0f));
|
||||
value_adjust = std::max(textWidth(min, MINMAX_FONT), textWidth(max, MINMAX_FONT)) + 5;
|
||||
} else if (item->sig->type == cabana::Signal::Type::Multiplexed) {
|
||||
|
||||
char freq[64];
|
||||
snprintf(freq, sizeof(freq), "%.2g hz", item->sparkline.freq());
|
||||
ImRect freq_rect(rect.Min.x + 5, rect.Min.y, rect.Max.x, rect.Max.y);
|
||||
drawText(painter, freq_rect, freq, text_color, nullptr, LABEL_FONT, ImVec2(0.0f, 0.5f));
|
||||
value_adjust = textWidth(freq, LABEL_FONT) + 10;
|
||||
}
|
||||
|
||||
rect.Min.x += value_adjust;
|
||||
rect.Max.x -= button_size_.x;
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
|
||||
} else {
|
||||
|
||||
rect.Max.x -= button_size_.x;
|
||||
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::drawEditor(SignalModel::Item *item) {
|
||||
const bool take_focus = focus_item_ == item;
|
||||
if (take_focus) focus_item_ = nullptr;
|
||||
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Node || item->type == SignalModel::Item::Offset ||
|
||||
item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue ||
|
||||
item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) {
|
||||
ImGuiInputTextCallback validator = nullptr;
|
||||
if (item->type == SignalModel::Item::Name) validator = nameValidator;
|
||||
else if (item->type == SignalModel::Item::Node) validator = nodeValidator;
|
||||
else validator = doubleValidator;
|
||||
|
||||
drawLineEditor(item, validator, take_focus);
|
||||
} else if (item->type == SignalModel::Item::Size) {
|
||||
int v = item->sig->size;
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
bool changed = ImGui::InputInt("##editor", &v, 1, 100, ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (ImGui::IsItemDeactivated() && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
|
||||
open_item_ = nullptr;
|
||||
return;
|
||||
}
|
||||
if (ImGui::IsItemDeactivatedAfterEdit() || (changed && !ImGui::IsItemActive())) {
|
||||
queueCommit(item, std::clamp(v, 1, CAN_MAX_DATA_BYTES));
|
||||
}
|
||||
|
||||
if (ImGui::IsItemDeactivated() && (!ImGui::IsItemHovered() || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false) || ImGui::IsKeyPressed(ImGuiKey_Escape, false))) {
|
||||
open_item_ = nullptr;
|
||||
}
|
||||
} else if (item->type == SignalModel::Item::SignalType) {
|
||||
|
||||
if (combo_focused_ && (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
|
||||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) {
|
||||
open_item_ = nullptr;
|
||||
combo_focused_ = false;
|
||||
return;
|
||||
}
|
||||
std::vector<std::pair<std::string, int>> items;
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal);
|
||||
if (!dbc()->msg(model_.msgId())->multiplexor) {
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor);
|
||||
} else if (item->sig->type != cabana::Signal::Type::Multiplexor) {
|
||||
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed);
|
||||
}
|
||||
std::vector<const char *> names;
|
||||
int current = -1;
|
||||
for (int i = 0; i < items.size(); ++i) {
|
||||
names.push_back(items[i].first.c_str());
|
||||
if (items[i].second == (int)item->sig->type) current = i;
|
||||
}
|
||||
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, ImGui::GetID("##editor"));
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
if (ImGui::Combo("##editor", ¤t, names.data(), names.size())) {
|
||||
queueCommit(item, items[current].second);
|
||||
open_item_ = nullptr;
|
||||
}
|
||||
combo_focused_ = ImGui::IsItemFocused() || ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None);
|
||||
if (!take_focus && !combo_focused_) open_item_ = nullptr;
|
||||
} else if (item->type == SignalModel::Item::Desc) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Header, (ImU32)0);
|
||||
const bool clicked = ImGui::Selectable("##editor", false, 0, ImVec2(0, rowHeight()));
|
||||
ImGui::PopStyleColor();
|
||||
drawElidedText(ImGui::GetWindowDrawList(), ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), model_.valueText(item),
|
||||
highlightedTextColor(), false);
|
||||
if (clicked || take_focus) {
|
||||
desc_dlg_ = std::make_unique<ValueDescriptionDlg>(item->sig->val_desc);
|
||||
desc_dlg_->title = item->sig->name;
|
||||
desc_sig_ = item->sig;
|
||||
}
|
||||
} else {
|
||||
|
||||
drawLineEditor(item, nullptr, take_focus);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::commitEditor() {
|
||||
SignalModel::Item *item = editing_item_;
|
||||
std::string text = edit_text_;
|
||||
closeEditor();
|
||||
if (item && validateEditor(item, text) == ValidState::Acceptable) {
|
||||
queueCommit(item, text);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::closeEditor() {
|
||||
editing_item_ = open_item_ = focus_item_ = nullptr;
|
||||
editor_active_ = refocus_editor_ = enter_pressed_ = combo_focused_ = false;
|
||||
pending_commit_ = nullptr;
|
||||
}
|
||||
|
||||
|
||||
ValidState SignalView::validateEditor(const SignalModel::Item *item, std::string &text) {
|
||||
if (item->type == SignalModel::Item::Name) return validateName(text);
|
||||
if (item->type == SignalModel::Item::Node) return validateNodes(text);
|
||||
if (item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor ||
|
||||
item->type == SignalModel::Item::MultiplexValue || item->type == SignalModel::Item::Min ||
|
||||
item->type == SignalModel::Item::Max) {
|
||||
return validateDouble(text);
|
||||
}
|
||||
return ValidState::Acceptable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
void SignalView::drawLineEditor(SignalModel::Item *item, ImGuiInputTextCallback validator, bool take_focus) {
|
||||
const bool editing = editing_item_ == item;
|
||||
const bool was_active = editing && editor_active_;
|
||||
|
||||
std::string text = editing ? edit_text_ : model_.valueText(item);
|
||||
if (take_focus) ImGui::SetKeyboardFocusHere();
|
||||
if (editing && refocus_editor_) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
refocus_editor_ = false;
|
||||
}
|
||||
validatedInput("##editor", &text, validator, "", ImGuiInputTextFlags_AutoSelectAll);
|
||||
if (ImGui::IsItemActivated()) editing_item_ = item;
|
||||
if (editing_item_ != item) return;
|
||||
|
||||
edit_text_ = text;
|
||||
editor_active_ = ImGui::IsItemActive();
|
||||
if (was_active) {
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
|
||||
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
enter_pressed_ = false;
|
||||
return;
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) {
|
||||
enter_pressed_ = true;
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemDeactivated()) {
|
||||
const bool by_enter = std::exchange(enter_pressed_, false);
|
||||
if (!ImGui::IsItemDeactivatedAfterEdit()) {
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
} else if (validateEditor(item, edit_text_) == ValidState::Acceptable) {
|
||||
queueCommit(item, edit_text_);
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
} else if (by_enter) {
|
||||
refocus_editor_ = true;
|
||||
} else {
|
||||
editing_item_ = open_item_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::queueCommit(SignalModel::Item *item, const ItemValue &value) {
|
||||
pending_commit_ = [this, item, value]() { model_.setData(item, value); };
|
||||
}
|
||||
|
||||
void SignalView::drawValueDescriptionDlg() {
|
||||
if (!desc_dlg_) return;
|
||||
if (desc_dlg_->draw()) return;
|
||||
|
||||
if (desc_dlg_->accepted) {
|
||||
|
||||
for (auto sig_item : model_.root()->children) {
|
||||
if (sig_item->sig != desc_sig_) continue;
|
||||
for (auto child : sig_item->children) {
|
||||
if (child->type != SignalModel::Item::ExtraInfo) continue;
|
||||
for (auto extra : child->children) {
|
||||
if (extra->type == SignalModel::Item::Desc) queueCommit(extra, desc_dlg_->val_desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
desc_dlg_.reset();
|
||||
desc_sig_ = nullptr;
|
||||
}
|
||||
|
||||
SignalView::SignalView(ChartsWidget *charts) : charts_(charts) {
|
||||
settings.sparkline_range = std::clamp(settings.sparkline_range, 1, SPARKLINE_RANGE_MAX);
|
||||
|
||||
|
||||
|
||||
button_size_ = ImVec2(22 * 2 + TOOLBAR_ITEM_SPACING, 22);
|
||||
updateToolBar();
|
||||
|
||||
connections_.push_back(model_.rowsChanged.connect([this]() { rowsChanged(); }));
|
||||
|
||||
connections_.push_back(model_.modelReset.connect([this]() {
|
||||
closeEditor();
|
||||
|
||||
|
||||
if (first_visible_row_ != -1) {
|
||||
last_visible_row_ = std::min(model_.rowCount() - 1, last_visible_row_ - first_visible_row_);
|
||||
first_visible_row_ = 0;
|
||||
}
|
||||
}));
|
||||
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
|
||||
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
|
||||
|
||||
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) {
|
||||
if (desc_sig_ == sig) desc_sig_ = nullptr;
|
||||
if ((editing_item_ && editing_item_->sig == sig) || (open_item_ && open_item_->sig == sig) ||
|
||||
(focus_item_ && focus_item_->sig == sig)) closeEditor();
|
||||
handleSignalRemoved(sig);
|
||||
}));
|
||||
connections_.push_back(dbc()->fileChanged.connect([this]() {
|
||||
desc_sig_ = nullptr;
|
||||
closeEditor();
|
||||
handleSignalRemoved(nullptr);
|
||||
}));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
|
||||
}
|
||||
|
||||
std::string SignalView::whatsThis() const {
|
||||
return R"(
|
||||
<b>Signal view</b><br />
|
||||
)";
|
||||
}
|
||||
|
||||
void SignalView::setMessage(const MessageId &id) {
|
||||
filter_edit_.clear();
|
||||
model_.setMessage(id);
|
||||
}
|
||||
|
||||
void SignalView::rowsChanged() {
|
||||
updateToolBar();
|
||||
updateChartState();
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::rowClicked(SignalModel::Item *item) {
|
||||
if (item->type == SignalModel::Item::Sig || item->type == SignalModel::Item::ExtraInfo) {
|
||||
item->expanded = !item->expanded;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::selectSignal(const cabana::Signal *sig, bool expand) {
|
||||
if (int row = model_.signalRow(sig); row != -1) {
|
||||
auto item = model_.root()->children[row];
|
||||
if (expand) {
|
||||
item->expanded = !item->expanded;
|
||||
}
|
||||
scroll_to_sig_ = sig;
|
||||
current_sig_ = sig;
|
||||
current_type_ = SignalModel::Item::Sig;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::updateChartState() {
|
||||
for (auto item : model_.root()->children) {
|
||||
item->chart_opened = charts_->hasSignal(model_.msgId(), item->sig);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::signalHovered(const cabana::Signal *sig) {
|
||||
auto &children = model_.root()->children;
|
||||
for (int i = 0; i < children.size(); ++i) {
|
||||
children[i]->highlight = children[i]->sig == sig;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::updateToolBar() {
|
||||
signal_count_lb_ = "Signals: " + std::to_string(model_.rowCount());
|
||||
sparkline_label_ = utils::formatSeconds(settings.sparkline_range);
|
||||
}
|
||||
|
||||
void SignalView::setSparklineRange(int value) {
|
||||
settings.sparkline_range = value;
|
||||
updateToolBar();
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
|
||||
if (id.address == model_.msgId().address) {
|
||||
selectSignal(sig);
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
|
||||
if (int row = model_.signalRow(sig); row != -1)
|
||||
updateState();
|
||||
}
|
||||
|
||||
void SignalView::handleSignalRemoved(const cabana::Signal *sig) {
|
||||
if (!sig || current_sig_ == sig) {
|
||||
|
||||
current_sig_ = nullptr;
|
||||
auto &children = model_.root()->children;
|
||||
if (sig && !children.empty() && current_row_ >= 0) {
|
||||
current_sig_ = children[std::min<int>(current_row_, children.size() - 1)]->sig;
|
||||
current_type_ = SignalModel::Item::Sig;
|
||||
}
|
||||
}
|
||||
if (!sig || scroll_to_sig_ == sig) scroll_to_sig_ = nullptr;
|
||||
if (!sig || hovered_sig_ == sig) hovered_sig_ = nullptr;
|
||||
}
|
||||
|
||||
float SignalView::widestValueWidth(const cabana::Signal *sig) {
|
||||
const double raw_max = sig->is_signed ? std::ldexp(1.0, sig->size - 1) - 1 : std::ldexp(1.0, sig->size) - 1;
|
||||
const double raw_min = sig->is_signed ? -std::ldexp(1.0, sig->size - 1) : 0.0;
|
||||
float width = 0;
|
||||
for (double raw : {raw_min, raw_max}) {
|
||||
width = std::max(width, textWidth(sig->formatValue(raw * sig->factor + sig->offset)));
|
||||
}
|
||||
for (const auto &[_, desc] : sig->val_desc) {
|
||||
width = std::max(width, textWidth(desc));
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
void SignalView::updateState(const std::set<MessageId> *msgs) {
|
||||
const auto &last_msg = can->lastMessage(model_.msgId());
|
||||
if (model_.rowCount() == 0 || (msgs && !msgs->count(model_.msgId())) || last_msg.dat.size() == 0) return;
|
||||
|
||||
|
||||
|
||||
float max_value_width = 0;
|
||||
for (auto item : model_.root()->children) {
|
||||
double value = 0;
|
||||
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
|
||||
item->sig_val = item->sig->formatValue(value);
|
||||
}
|
||||
max_value_width = std::max(max_value_width, widestValueWidth(item->sig));
|
||||
}
|
||||
|
||||
if (first_visible_row_ != -1 && last_visible_row_ != -1 && last_visible_row_ < model_.rowCount()) {
|
||||
const float min_max_width = textWidth("-000.00", MINMAX_FONT) + 5;
|
||||
float available_width = value_column_width_ - button_size_.x;
|
||||
float value_width = std::min<float>(max_value_width + min_max_width, available_width / 2);
|
||||
ImVec2 size(std::floor(available_width - value_width),
|
||||
std::floor(signalRowHeight() - V_MARGIN * 2));
|
||||
|
||||
|
||||
|
||||
|
||||
const double window_end = can->currentSec();
|
||||
|
||||
|
||||
const double lead_in = settings.sparkline_range * 0.05;
|
||||
|
||||
const auto range = can->eventsInRange(model_.msgId(), std::make_pair(window_end - settings.sparkline_range - lead_in, window_end));
|
||||
const CanEventIter first = range.first, last = range.second;
|
||||
std::vector<std::future<void>> futures;
|
||||
for (int i = first_visible_row_; i <= last_visible_row_; ++i) {
|
||||
auto item = model_.root()->children[i];
|
||||
futures.push_back(ThreadPool::instance().run([item, first, last, size, window_end]() {
|
||||
item->sparkline.update(item->sig, first, last, settings.sparkline_range, size, window_end);
|
||||
}));
|
||||
}
|
||||
for (auto &f : futures) f.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float SignalView::toolBarRightWidth(const std::string &range_label) {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
return ImGui::CalcTextSize(range_label.c_str()).x + style.ItemSpacing.x + SPARKLINE_SLIDER_WIDTH + style.ItemSpacing.x +
|
||||
ImGui::GetFont()->CalcTextSizeA(COLLAPSE_ICON_SIZE, FLT_MAX, 0.0f, icon::DASH_SQUARE).x + style.FramePadding.x * 2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
float SignalView::minimumWidth() {
|
||||
const ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float left_width = ImGui::CalcTextSize("Signals: 000").x + style.ItemSpacing.x + FILTER_WIDTH;
|
||||
|
||||
return left_width + style.ItemSpacing.x + toolBarRightWidth("00:00") + (style.WindowPadding.x + style.ChildBorderSize) * 2;
|
||||
}
|
||||
|
||||
void SignalView::draw() {
|
||||
if (!ImGui::BeginChild("SignalView", ImVec2(0, 0), ImGuiChildFlags_Borders)) {
|
||||
ImGui::EndChild();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(signal_count_lb_.c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(FILTER_WIDTH);
|
||||
if (clearableInput("##filter_edit", &filter_edit_, "Filter Signal", nonWhitespaceValidator)) {
|
||||
model_.setFilter(filter_edit_);
|
||||
}
|
||||
|
||||
|
||||
alignRight(toolBarRightWidth(sparkline_label_));
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(sparkline_label_.c_str());
|
||||
ImGui::SameLine();
|
||||
int range = settings.sparkline_range;
|
||||
if (fusionSliderInt("##sparkline_range_slider", &range, 1, SPARKLINE_RANGE_MAX, SPARKLINE_SLIDER_WIDTH)) {
|
||||
setSparklineRange(range);
|
||||
}
|
||||
ImGui::SetItemTooltip("Sparkline time range");
|
||||
ImGui::SameLine();
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), COLLAPSE_ICON_SIZE);
|
||||
const bool collapse = toolButton("collapse_all", icon::DASH_SQUARE, "Collapse All");
|
||||
ImGui::PopFont();
|
||||
if (collapse) collapseAll();
|
||||
|
||||
drawTree();
|
||||
drawValueDescriptionDlg();
|
||||
|
||||
if (pending_commit_) std::exchange(pending_commit_, nullptr)();
|
||||
if (pending_action_) std::exchange(pending_action_, nullptr)();
|
||||
current_row_ = model_.signalRow(current_sig_);
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void SignalView::collapseAll() {
|
||||
commitEditor();
|
||||
for (auto item : model_.root()->children) {
|
||||
item->expanded = false;
|
||||
for (auto child : item->children) child->expanded = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SignalView::drawTree() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
const float min_height = std::max(ImGui::GetContentRegionAvail().y, 300.0f);
|
||||
const bool visible = ImGui::BeginChild("tree", ImVec2(0, min_height), ImGuiChildFlags_None);
|
||||
ImGui::PopStyleVar();
|
||||
if (visible) {
|
||||
DrawContext ctx{ImGui::GetWindowDrawList(), ImGui::GetCursorScreenPos().x, ImGui::GetContentRegionAvail().x, rowHeight()};
|
||||
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) editor_open_on_press_ = open_item_ != nullptr;
|
||||
|
||||
int first_visible = -1, last_visible = -1;
|
||||
auto &children = model_.root()->children;
|
||||
for (int i = 0; i < children.size(); ++i) {
|
||||
ctx.any_visible = false;
|
||||
const bool header_visible = drawItem(children[i], 0, ctx);
|
||||
if (header_visible && first_visible == -1) first_visible = i;
|
||||
if (ctx.any_visible) last_visible = i;
|
||||
}
|
||||
if (first_visible == -1 && last_visible != -1) last_visible = -1;
|
||||
|
||||
bool changed = first_visible != first_visible_row_ || last_visible != last_visible_row_;
|
||||
first_visible_row_ = first_visible;
|
||||
last_visible_row_ = last_visible;
|
||||
scroll_to_sig_ = nullptr;
|
||||
|
||||
if (ctx.name_width > 0) name_column_width_ = ctx.name_width;
|
||||
if (ctx.value_column_width > 0 && ctx.value_column_width != value_column_width_) {
|
||||
value_column_width_ = ctx.value_column_width;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) updateState();
|
||||
|
||||
|
||||
|
||||
if (!ctx.mouse_on_row && ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
current_sig_ = nullptr;
|
||||
current_type_ = SignalModel::Item::Root;
|
||||
}
|
||||
|
||||
if (ctx.hovered_sig != hovered_sig_) {
|
||||
hovered_sig_ = ctx.hovered_sig;
|
||||
highlight(hovered_sig_);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
|
||||
bool SignalView::drawItem(SignalModel::Item *item, int depth, DrawContext &ctx) {
|
||||
const bool selected = item->sig == current_sig_ && item->type == current_type_;
|
||||
const float row_height = item->type == SignalModel::Item::Sig ? signalRowHeight() : ctx.row_height;
|
||||
const ImVec2 row_min = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 row_max(row_min.x + ctx.width, row_min.y + row_height);
|
||||
const bool row_visible = ImGui::IsRectVisible(row_min, row_max);
|
||||
ctx.any_visible |= row_visible;
|
||||
|
||||
ImGui::PushID(item);
|
||||
ImGui::BeginDisabled(!SignalModel::isEnabled(item));
|
||||
const bool row_clicked = viewSelectable("##row", selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height));
|
||||
|
||||
|
||||
const float branch_x = row_min.x + depth * INDENTATION;
|
||||
const bool on_branch = !item->children.empty() && ImGui::GetMousePos().x >= branch_x &&
|
||||
ImGui::GetMousePos().x < branch_x + INDENTATION;
|
||||
if (row_clicked && on_branch) {
|
||||
item->expanded = !item->expanded;
|
||||
} else if (row_clicked) {
|
||||
current_sig_ = item->sig;
|
||||
current_type_ = item->type;
|
||||
|
||||
|
||||
closeEditor();
|
||||
if (SignalModel::isEditable(item) && ImGui::GetMousePos().x >= row_min.x + name_column_width_) {
|
||||
focus_item_ = open_item_ = item;
|
||||
}
|
||||
rowClicked(item);
|
||||
}
|
||||
if (item->type == SignalModel::Item::Sig && item->sig == scroll_to_sig_) {
|
||||
ImGui::SetScrollHereY(0.0f);
|
||||
scroll_to_sig_ = nullptr;
|
||||
}
|
||||
if (ImGui::IsMouseHoveringRect(row_min, row_max)) {
|
||||
ctx.mouse_on_row = true;
|
||||
if (ImGui::IsWindowHovered()) ctx.hovered_sig = item->sig;
|
||||
}
|
||||
|
||||
if (!item->children.empty()) {
|
||||
const float arrow_size = ImGui::GetFontSize() * 0.7f;
|
||||
ImGui::RenderArrow(ctx.draw_list, ImVec2(row_min.x + depth * INDENTATION + 4.0f, row_min.y + (row_height - arrow_size) * 0.5f),
|
||||
ImGui::GetColorU32(ImGuiCol_Text), item->expanded ? ImGuiDir_Down : ImGuiDir_Right, 0.7f);
|
||||
}
|
||||
|
||||
|
||||
const std::string text0 = nameText(item);
|
||||
ctx.name_width = std::max(ctx.name_width, nameColumnWidth(item, ctx.width, text0));
|
||||
const ImRect rect1(ImVec2(row_min.x + name_column_width_, row_min.y), row_max);
|
||||
ctx.value_column_width = rect1.GetWidth();
|
||||
|
||||
|
||||
|
||||
const bool editor_open = selected && open_item_ == item;
|
||||
if (row_visible || editor_open) {
|
||||
const ImRect rect0(ImVec2(row_min.x + (depth + 1) * INDENTATION, row_min.y), ImVec2(row_min.x + name_column_width_, row_max.y));
|
||||
paintCell(ctx.draw_list, rect0, item, 0, selected, text0, ctx.viewport_x);
|
||||
if (item->type == SignalModel::Item::Sig && ImGui::IsMouseHoveringRect(ImVec2(row_min.x, row_min.y), rect0.Max) &&
|
||||
ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
|
||||
ImGui::TextUnformatted(utils::stripHtml(utils::signalToolTip(item->sig)).c_str());
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (item->type == SignalModel::Item::Sig) {
|
||||
paintCell(ctx.draw_list, rect1, item, 1, selected, item->sig_val, ctx.viewport_x);
|
||||
drawIndexWidget(item, rect1);
|
||||
} else if (SignalModel::isCheckable(item)) {
|
||||
bool checked = item->type == SignalModel::Item::Endian ? item->sig->is_little_endian : item->sig->is_signed;
|
||||
ImGui::SetCursorScreenPos(ImVec2(rect1.Min.x + H_MARGIN, rect1.Min.y));
|
||||
if (checkBox("##check", &checked)) queueCommit(item, checked);
|
||||
} else if (SignalModel::isEditable(item) && editor_open) {
|
||||
|
||||
ImGui::SetCursorScreenPos(rect1.Min);
|
||||
ImGui::SetNextItemWidth(rect1.GetWidth());
|
||||
drawEditor(item);
|
||||
} else {
|
||||
paintCell(ctx.draw_list, rect1, item, 1, selected, model_.valueText(item), ctx.viewport_x);
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopID();
|
||||
ImGui::SetCursorScreenPos(ImVec2(row_min.x, row_max.y));
|
||||
|
||||
if (item->expanded) {
|
||||
for (auto child : item->children) drawItem(child, depth + 1, ctx);
|
||||
}
|
||||
return row_visible;
|
||||
}
|
||||
|
||||
void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) {
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 2.0f));
|
||||
const ImVec2 btn_size(ImGui::CalcTextSize(icon::GRAPH_UP).x + 6.0f, ImGui::GetFrameHeight());
|
||||
const ImVec2 size(btn_size.x * 2 + TOOLBAR_ITEM_SPACING, btn_size.y);
|
||||
ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - size.x, rect.Min.y + (rect.GetHeight() - size.y) * 0.5f));
|
||||
|
||||
const auto sig = item->sig;
|
||||
const bool checked = item->chart_opened;
|
||||
if (checked) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
|
||||
if (ImGui::Button((std::string(icon::GRAPH_UP) + "##plot").c_str(), btn_size) && !editor_open_on_press_) {
|
||||
item->chart_opened = !checked;
|
||||
showChart(model_.msgId(), sig, item->chart_opened, ImGui::GetIO().KeyShift);
|
||||
}
|
||||
if (checked) ImGui::PopStyleColor();
|
||||
ImGui::SetItemTooltip("%s", checked ? "Close Plot" : "Show Plot\nSHIFT click to add to previous opened plot");
|
||||
ImGui::SameLine(0.0f, TOOLBAR_ITEM_SPACING);
|
||||
if (ImGui::Button((std::string(icon::X) + "##remove").c_str(), btn_size) && !editor_open_on_press_) {
|
||||
pending_action_ = [this, sig]() { UndoStack::instance()->push(new RemoveSigCommand(model_.msgId(), sig)); };
|
||||
}
|
||||
ImGui::SetItemTooltip("Remove signal");
|
||||
ImGui::PopStyleVar();
|
||||
button_size_ = size;
|
||||
}
|
||||
|
||||
ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions) {
|
||||
for (auto &[val, desc] : descriptions) {
|
||||
table_.emplace_back(utils::toString(val), desc);
|
||||
}
|
||||
}
|
||||
|
||||
bool ValueDescriptionDlg::draw() {
|
||||
const std::string popup_id = title + "###ValueDescriptionDlg";
|
||||
if (!opened_) {
|
||||
ImGui::OpenPopup(popup_id.c_str());
|
||||
opened_ = true;
|
||||
}
|
||||
setNextDialogWindow(ImVec2(500.0f, 0.0f));
|
||||
bool open = true;
|
||||
|
||||
if (!ImGui::BeginPopupModal(popup_id.c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) return ImGui::IsPopupOpen(popup_id.c_str());
|
||||
|
||||
bool closing = false;
|
||||
if (ImGui::Button(icon::PLUS)) {
|
||||
table_.emplace_back("", "");
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(current_row_ == -1);
|
||||
if (ImGui::Button(icon::DASH) && current_row_ < table_.size()) {
|
||||
table_.erase(table_.begin() + current_row_);
|
||||
current_row_ = -1;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY;
|
||||
if (ImGui::BeginTable("table", 3, flags, ImVec2(0.0f, 300.0f))) {
|
||||
ImGui::TableSetupScrollFreeze(1, 1);
|
||||
|
||||
ImGui::TableSetupColumn("##row_number", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel,
|
||||
ImGui::CalcTextSize("000").x + ImGui::GetStyle().CellPadding.x * 2);
|
||||
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||
ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableHeadersRow();
|
||||
for (int row = 0; row < table_.size(); ++row) {
|
||||
ImGui::PushID(row);
|
||||
ImGui::TableNextRow();
|
||||
if (row == current_row_) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImGuiCol_Header));
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted(std::to_string(row + 1).c_str());
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (valueDescriptionEditor(0, &table_[row].first)) current_row_ = row;
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (valueDescriptionEditor(1, &table_[row].second)) current_row_ = row;
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
bool accept = false, reject = false;
|
||||
if (dialogButtons("OK", &accept, &reject)) {
|
||||
if (accept) save();
|
||||
closing = true;
|
||||
}
|
||||
if (!open) closing = true;
|
||||
if (closing) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
return !closing;
|
||||
}
|
||||
|
||||
void ValueDescriptionDlg::save() {
|
||||
for (int i = 0; i < table_.size(); ++i) {
|
||||
std::string val = utils::trimmed(table_[i].first);
|
||||
std::string desc = utils::trimmed(table_[i].second);
|
||||
if (!val.empty() && !desc.empty()) {
|
||||
val_desc.push_back({utils::toDouble(val), desc});
|
||||
}
|
||||
}
|
||||
accepted = true;
|
||||
}
|
||||
201
iqpilot/tools/cabana/ui/widgets/signalview.h
Normal file
201
iqpilot/tools/cabana/ui/widgets/signalview.h
Normal file
@@ -0,0 +1,201 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "tools/cabana/core/observable.h"
|
||||
#include "tools/cabana/ui/chart/chartswidget.h"
|
||||
#include "tools/cabana/ui/chart/sparkline.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
|
||||
class ItemValue {
|
||||
public:
|
||||
ItemValue(const std::string &s) : str_(s) {}
|
||||
ItemValue(int v) : str_(std::to_string(v)) {}
|
||||
ItemValue(bool v) : str_(v ? "1" : "0") {}
|
||||
ItemValue(const ValueDescription &v) : val_desc_(v) {}
|
||||
ItemValue(const char *) = delete;
|
||||
std::string toString() const { return str_; }
|
||||
int toInt() const { return utils::toInt(str_); }
|
||||
bool toBool() const { return str_ == "1"; }
|
||||
double toDouble() const { return utils::toDouble(str_); }
|
||||
const ValueDescription &toValueDescription() const { return val_desc_; }
|
||||
|
||||
private:
|
||||
std::string str_;
|
||||
ValueDescription val_desc_;
|
||||
};
|
||||
|
||||
class SignalModel {
|
||||
public:
|
||||
struct Item {
|
||||
enum Type {Root, Sig, Name, Size, Node, Endian, Signed, Offset, Factor, SignalType, MultiplexValue, ExtraInfo, Unit, Comment, Min, Max, Desc };
|
||||
~Item() { for (auto c : children) delete c; }
|
||||
inline int row() const {
|
||||
auto it = std::find(parent->children.begin(), parent->children.end(), this);
|
||||
return it != parent->children.end() ? std::distance(parent->children.begin(), it) : -1;
|
||||
}
|
||||
|
||||
Type type = Type::Root;
|
||||
Item *parent = nullptr;
|
||||
std::vector<Item *> children;
|
||||
|
||||
const cabana::Signal *sig = nullptr;
|
||||
std::string title;
|
||||
bool highlight = false;
|
||||
std::string sig_val = "-";
|
||||
Sparkline sparkline;
|
||||
bool expanded = false;
|
||||
bool chart_opened = false;
|
||||
};
|
||||
|
||||
SignalModel();
|
||||
Item *root() const { return root_.get(); }
|
||||
const MessageId &msgId() const { return msg_id_; }
|
||||
int rowCount() const { return root_->children.size(); }
|
||||
static bool isEnabled(const Item *item);
|
||||
static bool isEditable(const Item *item);
|
||||
static bool isCheckable(const Item *item);
|
||||
std::string valueText(const Item *item) const;
|
||||
bool setData(Item *item, const ItemValue &value);
|
||||
void setMessage(const MessageId &id);
|
||||
void setFilter(const std::string &txt);
|
||||
bool saveSignal(const cabana::Signal *origin_s, cabana::Signal &s);
|
||||
int signalRow(const cabana::Signal *sig) const;
|
||||
|
||||
Observable<> rowsChanged;
|
||||
Observable<> modelReset;
|
||||
|
||||
private:
|
||||
void insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig);
|
||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void handleSignalUpdated(const cabana::Signal *sig);
|
||||
void handleSignalRemoved(const cabana::Signal *sig);
|
||||
void handleMsgChanged(MessageId id);
|
||||
void refresh();
|
||||
|
||||
MessageId msg_id_;
|
||||
std::string filter_str_;
|
||||
std::unique_ptr<Item> root_;
|
||||
Connections connections_;
|
||||
};
|
||||
|
||||
|
||||
class ValueDescriptionDlg {
|
||||
public:
|
||||
ValueDescriptionDlg(const ValueDescription &descriptions);
|
||||
bool draw();
|
||||
ValueDescription val_desc;
|
||||
std::string title;
|
||||
bool accepted = false;
|
||||
|
||||
private:
|
||||
void save();
|
||||
std::vector<std::pair<std::string, std::string>> table_;
|
||||
int current_row_ = -1;
|
||||
bool opened_ = false;
|
||||
};
|
||||
|
||||
class SignalView {
|
||||
public:
|
||||
SignalView(ChartsWidget *charts);
|
||||
void setMessage(const MessageId &id);
|
||||
void draw();
|
||||
static float minimumWidth();
|
||||
void signalHovered(const cabana::Signal *sig);
|
||||
void updateChartState();
|
||||
void selectSignal(const cabana::Signal *sig, bool expand = false);
|
||||
bool saveSignal(const cabana::Signal *origin, cabana::Signal &s) { return model_.saveSignal(origin, s); }
|
||||
std::string whatsThis() const;
|
||||
|
||||
Observable<const cabana::Signal *> highlight;
|
||||
Observable<const MessageId &, const cabana::Signal *, bool, bool> showChart;
|
||||
|
||||
private:
|
||||
void rowsChanged();
|
||||
void rowClicked(SignalModel::Item *item);
|
||||
static float toolBarRightWidth(const std::string &range_label);
|
||||
void updateToolBar();
|
||||
void setSparklineRange(int value);
|
||||
void handleSignalAdded(MessageId id, const cabana::Signal *sig);
|
||||
void handleSignalUpdated(const cabana::Signal *sig);
|
||||
void handleSignalRemoved(const cabana::Signal *sig);
|
||||
void updateState(const std::set<MessageId> *msgs = nullptr);
|
||||
|
||||
struct DrawContext {
|
||||
ImDrawList *draw_list;
|
||||
float viewport_x;
|
||||
float width;
|
||||
float row_height;
|
||||
float name_width = 0;
|
||||
float value_column_width = 0;
|
||||
bool any_visible = false;
|
||||
bool mouse_on_row = false;
|
||||
const cabana::Signal *hovered_sig = nullptr;
|
||||
};
|
||||
void drawTree();
|
||||
bool drawItem(SignalModel::Item *item, int depth, DrawContext &ctx);
|
||||
void drawIndexWidget(SignalModel::Item *item, const ImRect &rect);
|
||||
void collapseAll();
|
||||
static float widestValueWidth(const cabana::Signal *sig);
|
||||
|
||||
|
||||
void paintCell(ImDrawList *painter, const ImRect &rect, const SignalModel::Item *item, int column, bool selected,
|
||||
const std::string &text, float viewport_x) const;
|
||||
float nameColumnWidth(const SignalModel::Item *item, float widget_width, const std::string &text) const;
|
||||
|
||||
void drawEditor(SignalModel::Item *item);
|
||||
|
||||
|
||||
void queueCommit(SignalModel::Item *item, const ItemValue &value);
|
||||
void drawValueDescriptionDlg();
|
||||
static float textWidth(const std::string &text, float font_size = 0);
|
||||
void closeEditor();
|
||||
void commitEditor();
|
||||
|
||||
void drawLineEditor(SignalModel::Item *item, ImGuiInputTextCallback validator, bool take_focus);
|
||||
static ValidState validateEditor(const SignalModel::Item *item, std::string &text);
|
||||
|
||||
float value_column_width_ = 0;
|
||||
float name_column_width_ = 150;
|
||||
bool editor_open_on_press_ = false;
|
||||
|
||||
|
||||
int first_visible_row_ = -1;
|
||||
int last_visible_row_ = -1;
|
||||
const cabana::Signal *current_sig_ = nullptr;
|
||||
int current_row_ = -1;
|
||||
SignalModel::Item::Type current_type_ = SignalModel::Item::Root;
|
||||
const cabana::Signal *scroll_to_sig_ = nullptr;
|
||||
const cabana::Signal *hovered_sig_ = nullptr;
|
||||
std::function<void()> pending_action_;
|
||||
std::string sparkline_label_;
|
||||
std::string filter_edit_;
|
||||
ChartsWidget *charts_;
|
||||
std::string signal_count_lb_;
|
||||
|
||||
ImVec2 button_size_ = {};
|
||||
|
||||
SignalModel::Item *focus_item_ = nullptr;
|
||||
|
||||
SignalModel::Item *open_item_ = nullptr;
|
||||
std::function<void()> pending_commit_;
|
||||
SignalModel::Item *editing_item_ = nullptr;
|
||||
std::string edit_text_;
|
||||
bool editor_active_ = false;
|
||||
bool refocus_editor_ = false;
|
||||
bool enter_pressed_ = false;
|
||||
bool combo_focused_ = false;
|
||||
std::unique_ptr<ValueDescriptionDlg> desc_dlg_;
|
||||
const cabana::Signal *desc_sig_ = nullptr;
|
||||
SignalModel model_;
|
||||
Connections connections_;
|
||||
};
|
||||
92
iqpilot/tools/cabana/ui/widgets/tabbar.cc
Normal file
92
iqpilot/tools/cabana/ui/widgets/tabbar.cc
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
#include "tools/cabana/ui/widgets/scrollabletabbar.h"
|
||||
|
||||
int TabBar::addTab(const std::string &text) {
|
||||
tabs_.push_back({text, 0, next_id_++});
|
||||
int index = count() - 1;
|
||||
if (current_index_ == -1) {
|
||||
current_index_ = index;
|
||||
select_current_ = true;
|
||||
currentChanged(index);
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void TabBar::setCurrentIndex(int index) {
|
||||
if (index == current_index_ || index < -1 || index >= count()) return;
|
||||
current_index_ = index;
|
||||
select_current_ = true;
|
||||
currentChanged(index);
|
||||
}
|
||||
|
||||
int TabBar::tabAt(const ImVec2 &pos) const {
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (tabs_[i].rect.Contains(pos)) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TabBar::removeTab(int index) {
|
||||
tabs_.erase(tabs_.begin() + index);
|
||||
if (index == current_index_) {
|
||||
|
||||
current_index_ = count() ? std::min(index, count() - 1) : -1;
|
||||
select_current_ = true;
|
||||
currentChanged(current_index_);
|
||||
} else if (index < current_index_) {
|
||||
--current_index_;
|
||||
}
|
||||
}
|
||||
|
||||
void TabBar::moveTab(int from, int to) {
|
||||
if (from == to || from < 0 || from >= count() || to < 0 || to >= count()) return;
|
||||
const int current_id = current_index_ >= 0 ? tabs_[current_index_].id : -1;
|
||||
Tab tab = std::move(tabs_[from]);
|
||||
tabs_.erase(tabs_.begin() + from);
|
||||
tabs_.insert(tabs_.begin() + to, std::move(tab));
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
if (tabs_[i].id == current_id) current_index_ = i;
|
||||
}
|
||||
select_current_ = true;
|
||||
}
|
||||
|
||||
void TabBar::draw() {
|
||||
if (auto_hide_ && count() < 2) return;
|
||||
ImGui::PushID(this);
|
||||
|
||||
if (!(scroll_buttons_ ? beginScrollableTabBar("##tabbar", ImGuiTabBarFlags_NoTooltip) : ImGui::BeginTabBar("##tabbar", ImGuiTabBarFlags_NoTooltip))) {
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
const float close_button_min_width = tabs_closable_ ? std::exchange(style.TabCloseButtonMinWidthUnselected, -1.0f) : 0.0f;
|
||||
|
||||
const bool select_current = std::exchange(select_current_, false);
|
||||
int close_index = -1;
|
||||
for (int i = 0; i < count(); ++i) {
|
||||
bool open = true;
|
||||
const std::string label = tabs_[i].text + "###tab" + std::to_string(tabs_[i].id);
|
||||
const ImGuiTabItemFlags flags = (select_current && i == current_index_) ? ImGuiTabItemFlags_SetSelected : 0;
|
||||
if (ImGui::BeginTabItem(label.c_str(), tabs_closable_ ? &open : nullptr, flags)) {
|
||||
|
||||
if (!select_current && i != current_index_) {
|
||||
current_index_ = i;
|
||||
currentChanged(i);
|
||||
}
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
tabs_[i].rect = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
if (!tabs_[i].tooltip.empty()) ImGui::SetItemTooltip("%s", tabs_[i].tooltip.c_str());
|
||||
tabContextMenu(i);
|
||||
if (!open) close_index = i;
|
||||
}
|
||||
if (tabs_closable_) style.TabCloseButtonMinWidthUnselected = close_button_min_width;
|
||||
scroll_buttons_ ? endScrollableTabBar() : ImGui::EndTabBar();
|
||||
ImGui::PopID();
|
||||
if (close_index >= 0) tabCloseRequested(close_index);
|
||||
}
|
||||
45
iqpilot/tools/cabana/ui/widgets/tabbar.h
Normal file
45
iqpilot/tools/cabana/ui/widgets/tabbar.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/core/observable.h"
|
||||
|
||||
|
||||
class TabBar {
|
||||
public:
|
||||
TabBar() = default;
|
||||
int addTab(const std::string &text);
|
||||
int count() const { return (int)tabs_.size(); }
|
||||
void setTabText(int index, const std::string &text) { if (index >= 0 && index < count()) tabs_[index].text = text; }
|
||||
const std::string &tabText(int index) const { return tabs_[index].text; }
|
||||
void setTabToolTip(int index, const std::string &tip) { if (index >= 0 && index < count()) tabs_[index].tooltip = tip; }
|
||||
void setTabData(int index, int data) { if (index >= 0 && index < count()) tabs_[index].data = data; }
|
||||
int tabData(int index) const { return index >= 0 && index < count() ? tabs_[index].data : 0; }
|
||||
int currentIndex() const { return current_index_; }
|
||||
void setCurrentIndex(int index);
|
||||
int tabAt(const ImVec2 &pos) const;
|
||||
void removeTab(int index);
|
||||
void moveTab(int from, int to);
|
||||
void setAutoHide(bool hide) { auto_hide_ = hide; }
|
||||
void setTabsClosable(bool closable) { tabs_closable_ = closable; }
|
||||
void setUsesScrollButtons(bool use) { scroll_buttons_ = use; }
|
||||
void draw();
|
||||
|
||||
Observable<int> currentChanged;
|
||||
Observable<int> tabCloseRequested;
|
||||
Observable<int> tabContextMenu;
|
||||
|
||||
private:
|
||||
struct Tab { std::string text; int data = 0; int id = 0; std::string tooltip; ImRect rect; };
|
||||
std::vector<Tab> tabs_;
|
||||
int current_index_ = -1;
|
||||
int next_id_ = 0;
|
||||
bool select_current_ = false;
|
||||
bool auto_hide_ = false;
|
||||
bool tabs_closable_ = false;
|
||||
bool scroll_buttons_ = false;
|
||||
};
|
||||
612
iqpilot/tools/cabana/ui/widgets/videowidget.cc
Normal file
612
iqpilot/tools/cabana/ui/widgets/videowidget.cc
Normal file
@@ -0,0 +1,612 @@
|
||||
#include "tools/cabana/ui/widgets/videowidget.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cfloat>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <iterator>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
}
|
||||
#include <capnp/serialize.h>
|
||||
|
||||
#include "tools/cabana/settings.h"
|
||||
#include "tools/cabana/ui/threadpool.h"
|
||||
#include "tools/cabana/ui/util.h"
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
const int MIN_VIDEO_HEIGHT = 100;
|
||||
const int THUMBNAIL_MARGIN = 3;
|
||||
const float POINT_10_FONT_SIZE = 13.0f;
|
||||
const float POINT_16_FONT_SIZE = 21.0f;
|
||||
const float TOOLBAR_MARGIN_Y = 6.0f;
|
||||
const float TOOLBAR_SEPARATOR_EXTENT = 6.0f;
|
||||
const float SLIDER_HEIGHT = 15.0f;
|
||||
|
||||
|
||||
static const ImU32 timeline_colors[] = {
|
||||
IM_COL32(111, 143, 175, 255),
|
||||
IM_COL32(0, 163, 108, 255),
|
||||
IM_COL32(0, 255, 0, 255),
|
||||
IM_COL32(255, 195, 0, 255),
|
||||
IM_COL32(199, 0, 57, 255),
|
||||
IM_COL32(255, 0, 255, 255),
|
||||
};
|
||||
|
||||
static const float speeds[] = {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.};
|
||||
static const int NORMAL_SPEED_INDEX = std::find(std::begin(speeds), std::end(speeds), 1.0f) - std::begin(speeds);
|
||||
|
||||
static Replay *getReplay() {
|
||||
auto stream = dynamic_cast<ReplayStream *>(can);
|
||||
return stream ? stream->getReplay() : nullptr;
|
||||
}
|
||||
|
||||
static std::string colorName(ImU32 c) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "#%02x%02x%02x", (c >> IM_COL32_R_SHIFT) & 0xff, (c >> IM_COL32_G_SHIFT) & 0xff, (c >> IM_COL32_B_SHIFT) & 0xff);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
static std::pair<double, double> displayedTimeRange() {
|
||||
return can->timeRange().value_or(std::make_pair(can->minSeconds(), can->maxSeconds()));
|
||||
}
|
||||
|
||||
|
||||
static bool decodeJpeg(const uint8_t *data, size_t size, RgbImage *out) {
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
|
||||
AVCodecContext *context = codec ? avcodec_alloc_context3(codec) : nullptr;
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
bool ok = false;
|
||||
if (context && frame && packet && size > 0 && size <= (size_t)INT32_MAX && av_new_packet(packet, (int)size) >= 0) {
|
||||
std::copy(data, data + size, packet->data);
|
||||
ok = avcodec_open2(context, codec, nullptr) >= 0 && avcodec_send_packet(context, packet) >= 0 &&
|
||||
avcodec_receive_frame(context, frame) >= 0 && frame->width > 0 && frame->height > 0;
|
||||
}
|
||||
int chroma_x_shift = 0, chroma_y_shift = 0;
|
||||
if (ok) {
|
||||
switch ((AVPixelFormat)frame->format) {
|
||||
case AV_PIX_FMT_YUV420P: case AV_PIX_FMT_YUVJ420P: chroma_x_shift = chroma_y_shift = 1; break;
|
||||
case AV_PIX_FMT_YUV422P: case AV_PIX_FMT_YUVJ422P: chroma_x_shift = 1; break;
|
||||
case AV_PIX_FMT_YUV444P: case AV_PIX_FMT_YUVJ444P: break;
|
||||
default: ok = false; break;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
out->resize(frame->width, frame->height);
|
||||
const bool full_range = frame->color_range == AVCOL_RANGE_JPEG || frame->format == AV_PIX_FMT_YUVJ420P ||
|
||||
frame->format == AV_PIX_FMT_YUVJ422P || frame->format == AV_PIX_FMT_YUVJ444P;
|
||||
const float y_scale = full_range ? 1.0f : 1.164383f;
|
||||
const float y_offset = full_range ? 0.0f : 16.0f;
|
||||
const float kr = full_range ? 1.402f : 1.596027f;
|
||||
const float kgu = full_range ? 0.344136f : 0.391762f;
|
||||
const float kgv = full_range ? 0.714136f : 0.812968f;
|
||||
const float kb = full_range ? 1.772f : 2.017232f;
|
||||
for (int y = 0; y < frame->height; ++y) {
|
||||
const uint8_t *y_row = frame->data[0] + y * frame->linesize[0];
|
||||
const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1];
|
||||
const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2];
|
||||
uint8_t *dst = out->data.data() + (size_t)y * out->bytesPerLine();
|
||||
for (int x = 0; x < frame->width; ++x) {
|
||||
const float luma = y_scale * ((float)y_row[x] - y_offset);
|
||||
const float u = (float)u_row[x >> chroma_x_shift] - 128.0f;
|
||||
const float v = (float)v_row[x >> chroma_x_shift] - 128.0f;
|
||||
const float r = luma + kr * v;
|
||||
const float g = luma - kgu * u - kgv * v;
|
||||
const float b = luma + kb * u;
|
||||
dst[x * 4 + 0] = (uint8_t)std::clamp(std::lround(r), 0L, 255L);
|
||||
dst[x * 4 + 1] = (uint8_t)std::clamp(std::lround(g), 0L, 255L);
|
||||
dst[x * 4 + 2] = (uint8_t)std::clamp(std::lround(b), 0L, 255L);
|
||||
dst[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return ok;
|
||||
}
|
||||
|
||||
VideoWidget::VideoWidget() {
|
||||
if (!can->liveStreaming())
|
||||
createCameraWidget();
|
||||
|
||||
createSpeedDropdown();
|
||||
|
||||
connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { timeRangeChanged(); }));
|
||||
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *, bool) { msgs_received_ = true; }));
|
||||
}
|
||||
|
||||
std::string VideoWidget::whatsThis() const {
|
||||
|
||||
return "<b>Video</b><br />\n"
|
||||
"<span style=\"color:gray\">Timeline color</span><br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::None]) + " Disengaged " +
|
||||
colorName(timeline_colors[(int)TimelineType::Engaged]) + " Engaged<br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::UserBookmark]) + " User Flag " +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertInfo]) + " Info<br />\n" +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertWarning]) + " Warning " +
|
||||
colorName(timeline_colors[(int)TimelineType::AlertCritical]) + " Critical<br />\n"
|
||||
"<span style=\"color:gray\">Shortcuts</span><br />\n"
|
||||
"Pause/Resume: <span style=\"background-color:lightGray;color:gray\"> space </span>";
|
||||
}
|
||||
|
||||
static float toolbarHeight() { return TOOLBAR_MARGIN_Y + ImGui::GetFrameHeight(); }
|
||||
|
||||
void VideoWidget::drawPlaybackController() {
|
||||
beginToolbar();
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + TOOLBAR_MARGIN_Y);
|
||||
const float speed_width = menuButtonWidth("0.05x ", true);
|
||||
|
||||
const char *play_icon = can->isPaused() ? icon::PLAY : icon::PAUSE;
|
||||
const char *play_tooltip = can->isPaused() ? "Play" : "Pause";
|
||||
const char *loop_icon = getReplay() && getReplay()->loop() ? icon::REPEAT : icon::REPEAT_1;
|
||||
const std::string time_text = slider_ ? formatTime(can->currentSec(), true) + " / " + formatTime(slider_->maximum() / slider_->factor)
|
||||
: formatTime(can->currentSec(), true);
|
||||
const char *time_tooltip = settings.absolute_time ? "Elapsed time" : "Absolute time";
|
||||
|
||||
auto seek_backward = []() { can->seekTo(can->currentSec() - 1); };
|
||||
auto toggle_play = []() { can->pause(!can->isPaused()); };
|
||||
auto seek_forward = []() { can->seekTo(can->currentSec() + 1); };
|
||||
|
||||
std::vector<ToolbarItem> items = {
|
||||
{toolbarButtonWidth(icon::REWIND), [&]() { if (toolButton("rewind", icon::REWIND, "Seek backward")) seek_backward(); },
|
||||
"Seek backward", seek_backward},
|
||||
{toolbarButtonWidth(play_icon), [&]() { if (toolButton("play", play_icon, play_tooltip)) toggle_play(); },
|
||||
play_tooltip, toggle_play},
|
||||
{toolbarButtonWidth(icon::FAST_FORWARD), [&]() { if (toolButton("fast-forward", icon::FAST_FORWARD, "Seek forward")) seek_forward(); },
|
||||
"Seek forward", seek_forward},
|
||||
};
|
||||
if (can->liveStreaming()) {
|
||||
items.push_back({toolbarButtonWidth(icon::SKIP_END), [&]() {
|
||||
ImGui::BeginDisabled(!skip_to_end_enabled_);
|
||||
if (toolButton("skip-end", icon::SKIP_END, "Skip to the end")) skipToEnd();
|
||||
ImGui::EndDisabled();
|
||||
}, "Skip to the end", [this]() { skipToEnd(); }, skip_to_end_enabled_});
|
||||
}
|
||||
if (slider_ || msgs_received_) {
|
||||
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
const float time_width = toolbarButtonWidth(time_text);
|
||||
popMonoFont();
|
||||
items.push_back({time_width,
|
||||
[&]() {
|
||||
pushMonoFont(ImGui::GetFontSize());
|
||||
if (toolButton("time_display", time_text.c_str(), time_tooltip)) toggleTimeDisplay();
|
||||
popMonoFont();
|
||||
},
|
||||
time_text, [this]() { toggleTimeDisplay(); }});
|
||||
}
|
||||
|
||||
const size_t spacer_index = items.size();
|
||||
if (!can->liveStreaming()) {
|
||||
items.push_back({toolbarButtonWidth(loop_icon), [&]() { if (toolButton("loop", loop_icon, "Loop playback")) loopPlaybackClicked(); },
|
||||
"Loop playback", [this]() { loopPlaybackClicked(); }});
|
||||
}
|
||||
items.push_back({speed_width, [&]() { drawSpeedDropdown(speed_width); }});
|
||||
if (!can->liveStreaming()) {
|
||||
ToolbarItem separator{TOOLBAR_SEPARATOR_EXTENT, []() {
|
||||
|
||||
const ImVec2 min = ImGui::GetCursorScreenPos();
|
||||
ImGui::Dummy(ImVec2(TOOLBAR_SEPARATOR_EXTENT, ImGui::GetFrameHeight()));
|
||||
const float x = std::floor(min.x + TOOLBAR_SEPARATOR_EXTENT * 0.5f);
|
||||
ImGui::GetWindowDrawList()->AddLine(ImVec2(x, min.y + 4.0f), ImVec2(x, min.y + ImGui::GetFrameHeight() - 4.0f), ImGui::GetColorU32(ImGuiCol_Separator));
|
||||
}};
|
||||
separator.in_menu = false;
|
||||
items.push_back(std::move(separator));
|
||||
items.push_back({toolbarButtonWidth(icon::INFO_CIRCLE),
|
||||
[&]() { if (toolButton("route_info", icon::INFO_CIRCLE, "View route details")) showRouteInfo(); },
|
||||
"View route details", [this]() { showRouteInfo(); }});
|
||||
}
|
||||
|
||||
drawToolbar(items, spacer_index);
|
||||
endToolbar();
|
||||
}
|
||||
|
||||
void VideoWidget::skipToEnd() {
|
||||
|
||||
speed_index_ = NORMAL_SPEED_INDEX;
|
||||
can->pause(false);
|
||||
can->seekTo(can->maxSeconds() + 1);
|
||||
}
|
||||
|
||||
void VideoWidget::toggleTimeDisplay() {
|
||||
settings.absolute_time = !settings.absolute_time;
|
||||
}
|
||||
|
||||
static std::string speedText(float speed, const char *suffix) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%gx%s", speed, suffix);
|
||||
return buf;
|
||||
}
|
||||
|
||||
void VideoWidget::createSpeedDropdown() {
|
||||
speed_index_ = NORMAL_SPEED_INDEX;
|
||||
can->setSpeed(speeds[speed_index_]);
|
||||
speed_text_ = speedText(speeds[speed_index_], " ");
|
||||
}
|
||||
|
||||
void VideoWidget::drawSpeedDropdown(float width) {
|
||||
menuButton("speed_btn", speed_text_, "speed_menu", true, width);
|
||||
if (ImGui::BeginPopup("speed_menu")) {
|
||||
drawSpeedMenuItems();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::drawSpeedMenuItems() {
|
||||
|
||||
|
||||
const float indent = ImGui::GetFontSize();
|
||||
float label_width = 0;
|
||||
for (int i = 0; i < (int)std::size(speeds); ++i) {
|
||||
label_width = std::max(label_width, ImGui::CalcTextSize(speedText(speeds[i], "").c_str()).x);
|
||||
}
|
||||
for (int i = 0; i < (int)std::size(speeds); ++i) {
|
||||
const float speed = speeds[i];
|
||||
if (radioMenuItem(speedText(speed, "").c_str(), speed_index_ == i, indent + label_width + indent)) {
|
||||
speed_index_ = i;
|
||||
can->setSpeed(speed);
|
||||
speed_text_ = speedText(speed, " ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::createCameraWidget() {
|
||||
camera_tab_ = std::make_unique<TabBar>();
|
||||
camera_tab_->setAutoHide(true);
|
||||
|
||||
cam_widget_ = std::make_unique<StreamCameraView>("camerad", VISION_STREAM_ROAD);
|
||||
|
||||
slider_ = std::make_unique<Slider>();
|
||||
slider_->setTimeRange(can->minSeconds(), can->maxSeconds());
|
||||
|
||||
connections_.push_back(slider_->sliderReleased.connect([this]() { can->seekTo(slider_->currentSecond()); }));
|
||||
connections_.push_back(cam_widget_->clicked.connect([]() { can->pause(!can->isPaused()); }));
|
||||
connections_.push_back(cam_widget_->availableStreamsUpdated.connect([this](std::set<VisionStreamType> streams) { vipcAvailableStreamsUpdated(streams); }));
|
||||
connections_.push_back(camera_tab_->currentChanged.connect([this](int index) {
|
||||
if (index != -1) cam_widget_->setStreamType((VisionStreamType)camera_tab_->tabData(index));
|
||||
}));
|
||||
connections_.push_back(static_cast<ReplayStream *>(can)->qLogLoaded.connect([this](std::shared_ptr<LogReader> qlog) { cam_widget_->parseQLog(qlog); }));
|
||||
}
|
||||
|
||||
void VideoWidget::drawCameraWidget() {
|
||||
camera_tab_->draw();
|
||||
|
||||
|
||||
const ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, avail.y - SLIDER_HEIGHT - toolbarHeight());
|
||||
cam_widget_->draw(ImVec2(avail.x, cam_height), thumbnail_display_time_);
|
||||
|
||||
if (!slider_->isSliderDown()) slider_->setCurrentSecond(can->currentSec());
|
||||
slider_->draw(thumbnail_display_time_);
|
||||
updateSliderThumbnail();
|
||||
}
|
||||
|
||||
void VideoWidget::vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams) {
|
||||
static const std::string stream_names[] = {"Road camera", "Driver camera", "Wide road camera"};
|
||||
for (int i = 0; i < streams.size(); ++i) {
|
||||
if (camera_tab_->count() <= i) {
|
||||
camera_tab_->addTab(std::string());
|
||||
}
|
||||
int type = *std::next(streams.begin(), i);
|
||||
camera_tab_->setTabText(i, stream_names[type]);
|
||||
camera_tab_->setTabData(i, type);
|
||||
}
|
||||
while (camera_tab_->count() > streams.size()) {
|
||||
camera_tab_->removeTab(camera_tab_->count() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void VideoWidget::loopPlaybackClicked() {
|
||||
getReplay()->setLoop(!getReplay()->loop());
|
||||
}
|
||||
|
||||
void VideoWidget::timeRangeChanged() {
|
||||
const auto time_range = can->timeRange();
|
||||
if (can->liveStreaming()) {
|
||||
skip_to_end_enabled_ = !time_range.has_value();
|
||||
return;
|
||||
}
|
||||
time_range ? slider_->setTimeRange(time_range->first, time_range->second)
|
||||
: slider_->setTimeRange(can->minSeconds(), can->maxSeconds());
|
||||
}
|
||||
|
||||
std::string VideoWidget::formatTime(double sec, bool include_milliseconds) {
|
||||
if (settings.absolute_time)
|
||||
sec += std::chrono::duration<double>(can->beginDateTime().time_since_epoch()).count();
|
||||
return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time);
|
||||
}
|
||||
|
||||
void VideoWidget::setVisible(bool visible) {
|
||||
if (cam_widget_) cam_widget_->setVisible(visible);
|
||||
}
|
||||
|
||||
void VideoWidget::showThumbnail(double seconds) {
|
||||
if (can->liveStreaming()) return;
|
||||
thumbnail_display_time_ = seconds;
|
||||
}
|
||||
|
||||
void VideoWidget::showRouteInfo() {
|
||||
|
||||
route_info_dlgs_.push_back(std::make_unique<RouteInfoDlg>());
|
||||
}
|
||||
|
||||
void VideoWidget::updateSliderThumbnail() {
|
||||
if (slider_->underMouse()) {
|
||||
auto [min_sec, max_sec] = displayedTimeRange();
|
||||
showThumbnail(min_sec + (ImGui::GetMousePos().x - slider_->rect().Min.x) * (max_sec - min_sec) / slider_->width());
|
||||
} else if (slider_->mouseLeft()) {
|
||||
showThumbnail(-1);
|
||||
}
|
||||
}
|
||||
|
||||
float VideoWidget::sizeHintHeight() const {
|
||||
|
||||
return MIN_VIDEO_HEIGHT + SLIDER_HEIGHT + toolbarHeight();
|
||||
}
|
||||
|
||||
|
||||
float VideoWidget::defaultHeight(float width) const {
|
||||
if (!cam_widget_) return toolbarHeight();
|
||||
const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, width / cam_widget_->frameAspectRatio());
|
||||
const float tab_height = camera_tab_->count() >= 2 ? ImGui::GetFrameHeight() : 0.0f;
|
||||
return cam_height + tab_height + SLIDER_HEIGHT + toolbarHeight();
|
||||
}
|
||||
|
||||
void VideoWidget::draw() {
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
|
||||
if (!can->liveStreaming())
|
||||
drawCameraWidget();
|
||||
|
||||
drawPlaybackController();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
for (auto it = route_info_dlgs_.begin(); it != route_info_dlgs_.end();) {
|
||||
it = (*it)->draw() ? it + 1 : route_info_dlgs_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::draw(double thumbnail_time) {
|
||||
ImGui::InvisibleButton("##slider", ImVec2(std::max(1.0f, ImGui::GetContentRegionAvail().x), SLIDER_HEIGHT));
|
||||
rect_ = ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
const bool hovered = ImGui::IsItemHovered();
|
||||
left_ = hovered_ && !hovered;
|
||||
hovered_ = hovered;
|
||||
|
||||
if (ImGui::IsItemActivated()) handleMousePress();
|
||||
if (slider_down_) {
|
||||
if (ImGui::IsItemActive()) {
|
||||
|
||||
setValue(pixelPosToRangeValue(ImGui::GetMousePos().x - click_offset_));
|
||||
} else {
|
||||
slider_down_ = false;
|
||||
sliderReleased();
|
||||
}
|
||||
}
|
||||
paint(thumbnail_time);
|
||||
}
|
||||
|
||||
ImRect Slider::handleRect() const {
|
||||
const float handle_width = SLIDER_LENGTH;
|
||||
const float handle_height = std::min(SLIDER_THICKNESS, rect_.GetHeight());
|
||||
const int range = std::max(1, maximum() - minimum());
|
||||
const float x = rect_.Min.x + (float)(value() - minimum()) / range * std::max(0.0f, width() - handle_width);
|
||||
const float y = rect_.GetCenter().y - handle_height / 2;
|
||||
return ImRect(ImVec2(x, y), ImVec2(x + handle_width, y + handle_height));
|
||||
}
|
||||
|
||||
|
||||
int Slider::pixelPosToRangeValue(float x) const {
|
||||
const float handle_width = SLIDER_LENGTH;
|
||||
const float span = std::max(1.0f, width() - handle_width);
|
||||
return minimum() + (int)std::lround((maximum() - minimum()) * std::clamp((x - rect_.Min.x) / span, 0.0f, 1.0f));
|
||||
}
|
||||
|
||||
void Slider::paint(double thumbnail_time) {
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
|
||||
ImRect handle_rect = handleRect();
|
||||
ImRect groove_rect = rect_;
|
||||
|
||||
|
||||
float handle_height = handle_rect.GetHeight();
|
||||
const float groove_height = std::ceil(handle_height * 0.5f);
|
||||
const float center_y = rect_.GetCenter().y;
|
||||
groove_rect.Min.y = std::floor(center_y - groove_height / 2);
|
||||
groove_rect.Max.y = groove_rect.Min.y + groove_height;
|
||||
|
||||
p->AddRectFilled(groove_rect.Min, groove_rect.Max, timeline_colors[(int)TimelineType::None]);
|
||||
|
||||
double min = minimum() / factor;
|
||||
double max = maximum() / factor;
|
||||
const double span = std::max(max - min, 1e-9);
|
||||
|
||||
auto fillRange = [&](double begin, double end, ImU32 color) {
|
||||
if (begin > max || end < min) return;
|
||||
|
||||
|
||||
|
||||
ImRect r = groove_rect;
|
||||
r.Min.x = rect_.Min.x + std::floor(((std::max(min, begin) - min) / span) * width());
|
||||
r.Max.x = rect_.Min.x + std::floor(((std::min(max, end) - min) / span) * width()) + 1.0f;
|
||||
p->AddRectFilled(r.Min, r.Max, color);
|
||||
};
|
||||
|
||||
if (auto replay = getReplay()) {
|
||||
for (const auto &entry : *replay->getTimeline()) {
|
||||
fillRange(entry.start_time, entry.end_time, timeline_colors[(int)entry.type]);
|
||||
}
|
||||
|
||||
ImU32 empty_color = ImGui::GetColorU32(ImGuiCol_WindowBg, 160 / 255.0f);
|
||||
const auto event_data = replay->getEventData();
|
||||
for (const auto &[n, _] : replay->route().segments()) {
|
||||
if (!event_data->isSegmentLoaded(n))
|
||||
fillRange(n * 60.0, (n + 1) * 60.0, empty_color);
|
||||
}
|
||||
}
|
||||
|
||||
drawSliderHandle(p, handle_rect);
|
||||
|
||||
if (thumbnail_time >= 0) {
|
||||
float left = rect_.Min.x + (float)((thumbnail_time - min) * width() / span) - 1;
|
||||
ImRect rc(ImVec2(left, rect_.Min.y + 1), ImVec2(left + 2, rect_.Max.y - 1));
|
||||
p->AddRectFilled(rc.Min, rc.Max, ImGui::GetColorU32(ImGuiCol_Header), 1.5f);
|
||||
}
|
||||
}
|
||||
|
||||
void Slider::handleMousePress() {
|
||||
|
||||
const ImRect handle_rect = handleRect();
|
||||
if (handle_rect.Contains(ImGui::GetMousePos())) {
|
||||
slider_down_ = true;
|
||||
click_offset_ = ImGui::GetMousePos().x - handle_rect.Min.x;
|
||||
return;
|
||||
}
|
||||
setValue(minimum() + (int)(((maximum() - minimum()) * (ImGui::GetMousePos().x - rect_.Min.x)) / width()));
|
||||
sliderReleased();
|
||||
}
|
||||
|
||||
StreamCameraView::StreamCameraView(std::string stream_name, VisionStreamType stream_type)
|
||||
: CameraWidget(stream_name, stream_type) {
|
||||
big_thumbnail_texture_.mipmap = true;
|
||||
}
|
||||
|
||||
StreamCameraView::~StreamCameraView() {
|
||||
for (auto &pending : pending_thumbnails_) pending.done.wait();
|
||||
}
|
||||
|
||||
void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
|
||||
auto thumbnails = std::make_shared<std::map<uint64_t, RgbImage>>();
|
||||
auto done = ThreadPool::instance().run([qlog, thumbnails]() {
|
||||
for (const Event &e : qlog->events) {
|
||||
if (e.which != cereal::Event::Which::THUMBNAIL) continue;
|
||||
capnp::FlatArrayMessageReader reader(e.data);
|
||||
auto thumb_data = reader.getRoot<cereal::Event>().getThumbnail();
|
||||
auto image_data = thumb_data.getThumbnail();
|
||||
if (RgbImage thumb; decodeJpeg(image_data.begin(), image_data.size(), &thumb)) {
|
||||
(*thumbnails)[thumb_data.getTimestampEof()] = std::move(thumb);
|
||||
}
|
||||
}
|
||||
});
|
||||
pending_thumbnails_.push_back({std::move(done), std::move(thumbnails)});
|
||||
}
|
||||
|
||||
void StreamCameraView::collectThumbnails() {
|
||||
for (auto it = pending_thumbnails_.begin(); it != pending_thumbnails_.end();) {
|
||||
if (it->done.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
for (auto &[ts, thumb] : *it->thumbnails) big_thumbnails_[ts] = std::move(thumb);
|
||||
it = pending_thumbnails_.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::draw(const ImVec2 &size, double thumbnail_time) {
|
||||
collectThumbnails();
|
||||
CameraWidget::draw(size);
|
||||
|
||||
ImDrawList *p = ImGui::GetWindowDrawList();
|
||||
bool scrubbing = false;
|
||||
if (thumbnail_time >= 0) {
|
||||
scrubbing = can->isPaused();
|
||||
scrubbing ? drawScrubThumbnail(p, thumbnail_time) : drawThumbnail(p, thumbnail_time);
|
||||
}
|
||||
if (auto alert = getReplay()->findAlertAtTime(scrubbing ? thumbnail_time : can->currentSec())) {
|
||||
drawAlert(p, rect(), *alert, ImGui::GetFontSize());
|
||||
}
|
||||
|
||||
if (can->isPaused()) {
|
||||
ImFont *font = boldFont();
|
||||
const char *text = "PAUSED";
|
||||
const ImVec2 text_size = font->CalcTextSizeA(POINT_16_FONT_SIZE, FLT_MAX, 0.0f, text);
|
||||
const ImVec2 center = rect().GetCenter();
|
||||
p->AddText(font, POINT_16_FONT_SIZE, ImVec2(center.x - text_size.x / 2, center.y - text_size.y / 2),
|
||||
IM_COL32(200, 200, 200, static_cast<int>(255 * 0.7f)), text);
|
||||
}
|
||||
}
|
||||
|
||||
const RgbImage *StreamCameraView::thumbnailAt(double sec, uint64_t *mono_time) {
|
||||
auto it = big_thumbnails_.lower_bound(can->toMonoTime(sec));
|
||||
if (it == big_thumbnails_.end()) return nullptr;
|
||||
if (big_thumbnail_texture_.id == 0 || big_thumbnail_texture_.key != it->first) {
|
||||
big_thumbnail_texture_.upload(it->second);
|
||||
big_thumbnail_texture_.key = it->first;
|
||||
}
|
||||
if (mono_time) *mono_time = it->first;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
void StreamCameraView::drawScrubThumbnail(ImDrawList *p, double sec) {
|
||||
p->AddRectFilled(rect().Min, rect().Max, IM_COL32(0, 0, 0, 255));
|
||||
if (const RgbImage *image = thumbnailAt(sec, nullptr)) {
|
||||
|
||||
const float scale = std::min(width() / image->width, height() / image->height);
|
||||
const ImVec2 scaled_size(std::floor(image->width * scale), std::floor(image->height * scale));
|
||||
const ImVec2 center = rect().GetCenter();
|
||||
const ImVec2 thumb_min(center.x - (int)(scaled_size.x / 2), center.y - (int)(scaled_size.y / 2));
|
||||
ImRect thumb_rect(thumb_min, ImVec2(thumb_min.x + scaled_size.x, thumb_min.y + scaled_size.y));
|
||||
p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max);
|
||||
drawTime(p, thumb_rect, sec);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::drawThumbnail(ImDrawList *p, double sec) {
|
||||
uint64_t mono_time = 0;
|
||||
if (const RgbImage *image = thumbnailAt(sec, &mono_time)) {
|
||||
|
||||
const int h = MIN_VIDEO_HEIGHT - THUMBNAIL_MARGIN * 2;
|
||||
const int w = std::max(1, (int)std::lround((double)image->width * h / image->height));
|
||||
auto [min_sec, max_sec] = displayedTimeRange();
|
||||
int pos = (sec - min_sec) * width() / (max_sec - min_sec);
|
||||
const int max_x = (int)width() - w - THUMBNAIL_MARGIN + 1;
|
||||
int x = std::clamp(pos - w / 2, THUMBNAIL_MARGIN, std::max(THUMBNAIL_MARGIN, max_x));
|
||||
int y = height() - h - THUMBNAIL_MARGIN;
|
||||
|
||||
ImRect thumb_rect(ImVec2(rect().Min.x + x, rect().Min.y + y), ImVec2(rect().Min.x + x + w, rect().Min.y + y + h));
|
||||
p->AddImage(big_thumbnail_texture_.ref(), thumb_rect.Min, thumb_rect.Max);
|
||||
p->AddRect(thumb_rect.Min, thumb_rect.Max, paletteBrightText(), 0.0f, 0, 2.0f);
|
||||
if (auto alert = getReplay()->findAlertAtTime(can->toSeconds(mono_time))) {
|
||||
drawAlert(p, thumb_rect, *alert, POINT_10_FONT_SIZE);
|
||||
}
|
||||
drawTime(p, thumb_rect, sec);
|
||||
}
|
||||
}
|
||||
|
||||
void StreamCameraView::drawTime(ImDrawList *p, const ImRect &rect, double seconds) {
|
||||
char text[32];
|
||||
snprintf(text, sizeof(text), "%.3f", seconds);
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const ImVec2 text_size = font->CalcTextSizeA(POINT_10_FONT_SIZE, FLT_MAX, 0.0f, text);
|
||||
|
||||
p->AddText(font, POINT_10_FONT_SIZE, ImVec2(rect.GetCenter().x - text_size.x / 2, rect.Max.y - THUMBNAIL_MARGIN - text_size.y),
|
||||
paletteBrightText(), text);
|
||||
}
|
||||
|
||||
void StreamCameraView::drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size) {
|
||||
const ImU32 pen = paletteBrightText();
|
||||
ImU32 color = withAlpha(timeline_colors[int(alert.type)], 128);
|
||||
std::string text = alert.text1;
|
||||
if (!alert.text2.empty()) text += "\n" + alert.text2;
|
||||
|
||||
ImRect text_rect(ImVec2(rect.Min.x + 1, rect.Min.y + 1), ImVec2(rect.Max.x - 1, rect.Max.y - 1));
|
||||
ImFont *font = ImGui::GetFont();
|
||||
const float wrap_width = std::max(1.0f, text_rect.GetWidth());
|
||||
const ImVec2 r = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text.c_str());
|
||||
p->AddRectFilled(ImVec2(text_rect.Min.x, text_rect.Min.y), ImVec2(text_rect.Max.x, text_rect.Min.y + r.y), color);
|
||||
|
||||
float y = text_rect.Min.y;
|
||||
for (const auto &line : utils::split(text, '\n')) {
|
||||
const ImVec2 line_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, line.c_str());
|
||||
p->AddText(font, font_size, ImVec2(text_rect.Min.x + (text_rect.GetWidth() - line_size.x) / 2, y), pen, line.c_str(), nullptr, wrap_width);
|
||||
y += line_size.y;
|
||||
}
|
||||
}
|
||||
121
iqpilot/tools/cabana/ui/widgets/videowidget.h
Normal file
121
iqpilot/tools/cabana/ui/widgets/videowidget.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
#include "tools/cabana/ui/widgets/cameraview.h"
|
||||
#include "tools/cabana/ui/widgets/tabbar.h"
|
||||
#include "tools/cabana/ui/tools/routeinfo.h"
|
||||
#include "tools/replay/logreader.h"
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
#include "tools/cabana/ui/icons.h"
|
||||
|
||||
class Slider {
|
||||
public:
|
||||
Slider() = default;
|
||||
double currentSecond() const { return value() / factor; }
|
||||
void setCurrentSecond(double sec) { setValue(sec * factor); }
|
||||
void setTimeRange(double min, double max) { setRange(min * factor, max * factor); }
|
||||
int value() const { return value_; }
|
||||
void setValue(int v) { value_ = std::clamp(v, minimum_, maximum_); }
|
||||
void setRange(int min, int max) { minimum_ = min; maximum_ = std::max(min, max); setValue(value_); }
|
||||
int minimum() const { return minimum_; }
|
||||
int maximum() const { return maximum_; }
|
||||
bool isSliderDown() const { return slider_down_; }
|
||||
float width() const { return rect_.GetWidth(); }
|
||||
const ImRect &rect() const { return rect_; }
|
||||
bool underMouse() const { return hovered_; }
|
||||
bool mouseLeft() const { return left_; }
|
||||
void draw(double thumbnail_time);
|
||||
static constexpr double factor = 1000.0;
|
||||
|
||||
Observable<> sliderReleased;
|
||||
|
||||
private:
|
||||
void handleMousePress();
|
||||
void paint(double thumbnail_time);
|
||||
ImRect handleRect() const;
|
||||
int pixelPosToRangeValue(float x) const;
|
||||
int minimum_ = 0;
|
||||
int maximum_ = 99;
|
||||
int value_ = 0;
|
||||
bool slider_down_ = false;
|
||||
float click_offset_ = 0;
|
||||
bool hovered_ = false;
|
||||
bool left_ = false;
|
||||
ImRect rect_;
|
||||
};
|
||||
|
||||
class StreamCameraView : public CameraWidget {
|
||||
public:
|
||||
StreamCameraView(std::string stream_name, VisionStreamType stream_type);
|
||||
~StreamCameraView();
|
||||
void draw(const ImVec2 &size, double thumbnail_time);
|
||||
void parseQLog(std::shared_ptr<LogReader> qlog);
|
||||
|
||||
private:
|
||||
struct PendingThumbnails {
|
||||
std::future<void> done;
|
||||
std::shared_ptr<std::map<uint64_t, RgbImage>> thumbnails;
|
||||
};
|
||||
void collectThumbnails();
|
||||
|
||||
const RgbImage *thumbnailAt(double sec, uint64_t *mono_time);
|
||||
void drawAlert(ImDrawList *p, const ImRect &rect, const Timeline::Entry &alert, float font_size);
|
||||
void drawThumbnail(ImDrawList *p, double sec);
|
||||
void drawScrubThumbnail(ImDrawList *p, double sec);
|
||||
void drawTime(ImDrawList *p, const ImRect &rect, double seconds);
|
||||
|
||||
std::map<uint64_t, RgbImage> big_thumbnails_;
|
||||
GlTexture big_thumbnail_texture_;
|
||||
std::vector<PendingThumbnails> pending_thumbnails_;
|
||||
};
|
||||
|
||||
class VideoWidget {
|
||||
public:
|
||||
VideoWidget();
|
||||
void draw();
|
||||
float sizeHintHeight() const;
|
||||
float defaultHeight(float width) const;
|
||||
|
||||
|
||||
void setVisible(bool visible);
|
||||
void showThumbnail(double seconds);
|
||||
std::string whatsThis() const;
|
||||
|
||||
private:
|
||||
void updateSliderThumbnail();
|
||||
std::string formatTime(double sec, bool include_milliseconds = false);
|
||||
void timeRangeChanged();
|
||||
void createCameraWidget();
|
||||
void drawCameraWidget();
|
||||
void drawPlaybackController();
|
||||
void skipToEnd();
|
||||
void toggleTimeDisplay();
|
||||
void createSpeedDropdown();
|
||||
void drawSpeedDropdown(float width);
|
||||
void drawSpeedMenuItems();
|
||||
void loopPlaybackClicked();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
void showRouteInfo();
|
||||
|
||||
std::unique_ptr<StreamCameraView> cam_widget_;
|
||||
std::string speed_text_;
|
||||
int speed_index_ = -1;
|
||||
bool skip_to_end_enabled_ = true;
|
||||
bool msgs_received_ = false;
|
||||
double thumbnail_display_time_ = -1;
|
||||
std::unique_ptr<Slider> slider_;
|
||||
std::unique_ptr<TabBar> camera_tab_;
|
||||
std::vector<std::unique_ptr<RouteInfoDlg>> route_info_dlgs_;
|
||||
Connections connections_;
|
||||
};
|
||||
Reference in New Issue
Block a user