Files
IQ.Pilot/iqpilot/tools/cabana/ui/widgets/messageswidget.cc
2026-09-02 16:46:45 -05:00

513 lines
18 KiB
C++

#include "tools/cabana/ui/widgets/messageswidget.h"
#include <algorithm>
#include <cctype>
#include <cfloat>
#include <charconv>
#include <cmath>
#include <cstdio>
#include <limits>
#include <numeric>
#include <utility>
#include "imgui_internal.h"
#include "tools/cabana/commands.h"
#include "tools/cabana/settings.h"
#include "tools/cabana/ui/icons.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/ui/widgets/messagebytes.h"
#include "tools/cabana/utils/strings.h"
namespace {
const char *COLUMN_TITLES[MessageList::COLUMN_COUNT] = {"Name", "Bus", "ID", "Node", "Freq", "Count", "Bytes"};
constexpr float DEFAULT_SECTION_SIZE = 100.0f;
unsigned int toUInt(const std::string &s, bool *ok, int base) {
const char *b = s.data(), *e = b + s.size();
while (b < e && std::isspace((unsigned char)*b)) ++b;
while (e > b && std::isspace((unsigned char)e[-1])) --e;
unsigned int v = 0;
auto [p, ec] = std::from_chars(b, e, v, base);
*ok = b < e && p == e && ec == std::errc();
return *ok ? v : 0;
}
bool parseRange(const std::string &filter, uint32_t value, int base = 10) {
unsigned int min = std::numeric_limits<unsigned int>::min();
unsigned int max = std::numeric_limits<unsigned int>::max();
auto s = utils::split(filter, '-');
bool ok = s.size() >= 1 && s.size() <= 2;
if (ok && !s[0].empty()) min = toUInt(s[0], &ok, base);
if (ok && s.size() == 1) {
max = min;
} else if (ok && s.size() == 2 && !s[1].empty()) {
max = toUInt(s[1], &ok, base);
}
return ok && value >= min && value <= max;
}
inline ImGuiSortDirection flipSortDirection(ImGuiSortDirection dir) {
return dir == ImGuiSortDirection_Ascending ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending;
}
std::string formatFreq(float freq) {
if (freq <= 0) return "--";
char buf[32];
snprintf(buf, sizeof(buf), freq >= 0.95 ? "%.0f" : "%.2f", freq >= 0.95 ? std::nearbyint(freq) : freq);
return buf;
}
std::string cellText(const MessageList::Item &item, int column) {
const bool seen = item.id.source != INVALID_SOURCE;
switch (column) {
case MessageList::NAME: return item.name;
case MessageList::SOURCE: return seen ? std::to_string(item.id.source) : "N/A";
case MessageList::ADDRESS: return utils::toHexString(item.id.address);
case MessageList::NODE: return item.node;
case MessageList::FREQ: return seen ? formatFreq(can->lastMessage(item.id).freq) : "N/A";
case MessageList::COUNT: return seen ? std::to_string(can->lastMessage(item.id).count) : "N/A";
case MessageList::DATA: return seen ? "" : "N/A";
}
return {};
}
}
MessageList::MessageList() {
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); }));
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); }));
connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); }));
}
void MessageList::setFilters(const std::map<int, std::string> &filters) {
filters_ = filters;
filterAndSort();
}
void MessageList::showInactiveMessages(bool show) {
show_inactive_messages = show;
filterAndSort();
}
void MessageList::dbcModified() {
dbc_messages_.clear();
for (const auto &[_, m] : dbc()->getMessages(-1)) {
dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address});
}
filterAndSort();
}
void MessageList::sortItems(std::vector<Item> &list) {
auto compare = [this](const auto &l, const auto &r) {
switch (sort_column_) {
case NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id);
case SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address);
case ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source);
case NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id);
case FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id);
case COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id);
default: return false;
}
};
if (sort_order_ == ImGuiSortDirection_Descending)
std::stable_sort(list.rbegin(), list.rend(), compare);
else
std::stable_sort(list.begin(), list.end(), compare);
}
bool MessageList::match(const Item &item) {
if (filters_.empty()) return true;
bool match = true;
const auto &data = can->lastMessage(item.id);
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
const std::string &txt = it->second;
switch (it->first) {
case NAME: {
match = utils::containsCI(item.name, txt);
if (!match) {
const auto m = dbc()->msg(item.id);
match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(),
[&txt](const auto &s) { return utils::containsCI(s->name, txt); });
}
break;
}
case SOURCE:
match = parseRange(txt, item.id.source);
break;
case ADDRESS:
match = utils::containsCI(utils::toHexString(item.id.address), txt);
match = match || parseRange(txt, item.id.address, 16);
break;
case NODE:
match = utils::containsCI(item.node, txt);
break;
case FREQ:
match = parseRange(txt, data.freq);
break;
case COUNT:
match = parseRange(txt, data.count);
break;
case DATA:
match = utils::containsCI(utils::toHex(data.dat), txt);
break;
}
}
return match;
}
bool MessageList::filterAndSort() {
std::vector<MessageId> all_messages;
all_messages.reserve(can->lastMessages().size() + dbc_messages_.size());
auto dbc_msgs = dbc_messages_;
for (const auto &[id, m] : can->lastMessages()) {
all_messages.push_back(id);
dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address});
}
all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end());
std::vector<Item> new_items;
new_items.reserve(all_messages.size());
for (const auto &id : all_messages) {
if (show_inactive_messages || can->isMessageActive(id)) {
auto msg = dbc()->msg(id);
Item item = {.id = id, .name = msg ? msg->name : UNTITLED, .node = msg ? msg->transmitter : std::string()};
if (match(item)) new_items.emplace_back(item);
}
}
sortItems(new_items);
if (items != new_items) {
items = std::move(new_items);
changed();
return true;
}
return false;
}
void MessageList::msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids) {
if (has_new_ids || ((filters_.count(FREQ) || filters_.count(COUNT) || filters_.count(DATA)) &&
++sort_threshold_ == STREAM_UPDATE_FPS)) {
sort_threshold_ = 0;
filterAndSort();
}
}
void MessageList::sort(int column, ImGuiSortDirection order) {
if (column != DATA) {
sort_column_ = column;
sort_order_ = order;
filterAndSort();
}
}
MessagesWidget::MessagesWidget() {
std::iota(display_order_.begin(), display_order_.end(), 0);
list_.sort(MessageList::NAME, ImGuiSortDirection_Ascending);
connections_.push_back(list_.changed.connect([this]() {
current_row_ = -1;
if (current_msg_id_) selectMessage(*current_msg_id_);
updateBytesSectionSize();
updateTitle();
}));
suppressHighlighted();
}
std::string MessagesWidget::whatsThis() const {
return R"(
<b>Message View</b><br/>
<span style="color:gray">Byte color</span><br />
<span style="color:gray;">&#9632; </span> constant changing<br />
<span style="color:blue;">&#9632; </span> increasing<br />
<span style="color:red;">&#9632; </span> decreasing<br />
<span style="color:gray">Shortcuts</span><br />
Horizontal Scrolling: <span style="background-color:lightGray;color:gray">&nbsp;shift+wheel&nbsp;</span>
)";
}
void MessagesWidget::drawToolBar() {
ImGui::Dummy(ImVec2(0, std::max(0.0f, 9 - ImGui::GetStyle().ItemSpacing.y)));
if (ImGui::Button("Suppress Highlighted")) suppressHighlighted(true);
ImGui::SameLine();
ImGui::BeginDisabled(!suppress_clear_enabled_);
const std::string clear_label = suppress_clear_text_ + "##suppress_clear";
if (ImGui::Button(clear_label.c_str())) suppressHighlighted(false);
ImGui::EndDisabled();
disabledItemTooltip("Clear suppressed");
const ImGuiStyle &style = ImGui::GetStyle();
const float checkbox_width = ImGui::CalcTextSize("Suppress Signals").x + ImGui::GetFrameHeight() + style.ItemInnerSpacing.x;
const float view_button_width = ImGui::CalcTextSize(icon::THREE_DOTS).x + style.FramePadding.x * 2;
alignRight(checkbox_width + style.ItemSpacing.x + view_button_width);
bool suppress_defined_signals = settings.suppress_defined_signals;
if (checkBox("Suppress Signals", &suppress_defined_signals)) can->suppressDefinedSignals(suppress_defined_signals);
ImGui::SetItemTooltip("Suppress defined signals");
ImGui::SameLine();
if (toolButton("view_btn", icon::THREE_DOTS, "View...")) ImGui::OpenPopup("menu");
}
void MessagesWidget::updateTitle() {
auto stats = std::accumulate(
list_.items.begin(), list_.items.end(), std::pair<size_t, size_t>(),
[](const auto &pair, const auto &item) {
auto m = dbc()->msg(item.id);
return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair;
});
char buf[128];
snprintf(buf, sizeof(buf), "%zu Messages (%zu DBC Messages, %zu Signals)", list_.items.size(), stats.first, stats.second);
title_ = buf;
}
void MessagesWidget::selectMessage(const MessageId &msg_id) {
auto it = std::find_if(list_.items.cbegin(), list_.items.cend(), [&msg_id](auto &item) { return item.id == msg_id; });
if (it != list_.items.cend()) setCurrentRow(std::distance(list_.items.cbegin(), it));
}
void MessagesWidget::setCurrentRow(int row) {
if (row < 0 || row >= (int)list_.items.size()) return;
current_row_ = row;
scroll_to_current_ = true;
const auto &id = list_.items[row].id;
if (!current_msg_id_ || id != *current_msg_id_) {
current_msg_id_ = id;
msgSelectionChanged(*current_msg_id_);
}
}
void MessagesWidget::suppressHighlighted(bool from_suppress_add) {
int n = from_suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0);
suppress_clear_text_ = n > 0 ? "Clear (" + std::to_string(n) + ")" : "Clear";
suppress_clear_enabled_ = n > 0;
}
void MessagesWidget::drawContextMenu() {
if (!ImGui::BeginPopup("menu")) return;
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
const int column = display_order_[i];
if (ImGui::MenuItem(COLUMN_TITLES[column], nullptr, !hidden_[column], column > 0)) {
pending_hidden_.emplace_back(column, !hidden_[column]);
}
}
ImGui::Separator();
if (ImGui::MenuItem("Multi-Line bytes", nullptr, settings.multiple_lines_hex)) {
setMultiLineBytes(!settings.multiple_lines_hex);
}
if (ImGui::MenuItem("Show inactive messages", nullptr, list_.show_inactive_messages)) {
list_.showInactiveMessages(!list_.show_inactive_messages);
}
ImGui::EndPopup();
}
void MessagesWidget::setMultiLineBytes(bool multi) {
settings.multiple_lines_hex = multi;
updateBytesSectionSize();
}
void MessagesWidget::updateBytesSectionSize() {
int max_bytes = 8;
if (!settings.multiple_lines_hex) {
for (const auto &[_, m] : can->lastMessages()) {
max_bytes = std::max<int>(max_bytes, m.dat.size());
}
}
bytes_section_bytes_ = max_bytes;
}
void MessagesWidget::draw() {
drawToolBar();
drawTable();
if (std::exchange(header_menu_requested_, false)) ImGui::OpenPopup("menu");
drawContextMenu();
}
void MessagesWidget::handleKeys() {
if (!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows) || ImGui::IsAnyItemActive()) return;
const int last = (int)list_.items.size() - 1;
if (last < 0) return;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && current_row_ > 0) {
setCurrentRow(current_row_ - 1);
} else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && current_row_ < last) {
setCurrentRow(current_row_ + 1);
} else if (ImGui::IsKeyPressed(ImGuiKey_Home)) {
setCurrentRow(0);
} else if (ImGui::IsKeyPressed(ImGuiKey_End)) {
setCurrentRow(last);
} else if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
setCurrentRow(std::max(current_row_ - visible_rows_, 0));
} else if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
setCurrentRow(std::min(current_row_ + visible_rows_, last));
}
}
void MessagesWidget::drawTable() {
handleKeys();
const bool multiple_lines = settings.multiple_lines_hex;
const ImGuiTableFlags flags = ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable |
ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders |
ImGuiTableFlags_Hideable;
const float bytes_width = bytesCellSize(bytes_section_bytes_, multiple_lines).x;
const float avail_width = ImGui::GetContentRegionAvail().x - (has_scrollbar_y_ ? ImGui::GetStyle().ScrollbarSize : 0);
const float inner_width = std::max(avail_width, fixed_columns_width_ + bytes_width);
if (!ImGui::BeginTable("messages", MessageList::COLUMN_COUNT, flags, ImVec2(0, 0), inner_width)) return;
ImGui::TableSetupScrollFreeze(0, 2);
ImGuiTable *table = ImGui::GetCurrentTable();
table->DisableDefaultContextMenu = true;
table->IsContextPopupOpen = false;
for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) {
ImGuiTableColumnFlags column_flags = ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_PreferSortDescending;
float width = DEFAULT_SECTION_SIZE;
if (i == MessageList::NAME) {
column_flags |= ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoHide;
} else if (i == MessageList::DATA) {
column_flags = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_NoResize;
width = 0;
}
ImGui::TableSetupColumn(COLUMN_TITLES[i], column_flags, width);
}
for (const auto &[column, hide] : pending_hidden_) ImGui::TableSetColumnEnabled(column, !hide);
pending_hidden_.clear();
if (ImGuiTableSortSpecs *specs = ImGui::TableGetSortSpecs(); specs && specs->SpecsDirty) {
if (specs->SpecsCount > 0) list_.sort(specs->Specs[0].ColumnIndex, flipSortDirection(specs->Specs[0].SortDirection));
specs->SpecsDirty = false;
if (current_row_ >= 0) scroll_to_current_ = true;
}
drawHeader();
const int rows = list_.items.size();
if (!multiple_lines) {
ImGuiListClipper clipper;
clipper.Begin(rows);
if (scroll_to_current_ && current_row_ >= 0) clipper.IncludeItemByIndex(current_row_);
while (clipper.Step()) {
for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) drawRow(row);
}
} else {
for (int row = 0; row < rows; ++row) drawRow(row);
}
const ImGuiTableColumn &data_column = table->Columns[MessageList::DATA];
fixed_columns_width_ = data_column.IsEnabled ? table->ColumnsGivenWidth - data_column.WidthGiven : 0;
has_scrollbar_y_ = table->InnerWindow->ScrollbarY;
visible_rows_ = std::max(1, (int)(table->InnerWindow->InnerRect.GetHeight() / bytesCellSize(0, multiple_lines).y) - 2);
ImGui::EndTable();
}
void MessagesWidget::drawHeader() {
if (tableHeadersRow() >= 0) header_menu_requested_ = true;
ImGuiTable *table = ImGui::GetCurrentTable();
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
display_order_[i] = table->DisplayOrderToIndex[i];
hidden_[i] = !(ImGui::TableGetColumnFlags(i) & ImGuiTableColumnFlags_IsEnabled);
}
const float clear_width = ImGui::CalcTextSize(icon::X).x + ImGui::GetStyle().FramePadding.x * 2;
ImGui::TableNextRow();
for (int i = 0; i < MessageList::COLUMN_COUNT; i++) {
if (!ImGui::TableSetColumnIndex(i)) continue;
ImGui::PushID(i);
ImGui::SetNextItemWidth(filters_[i].empty() ? -FLT_MIN : std::max(1.0f, ImGui::GetContentRegionAvail().x - clear_width));
const std::string placeholder = std::string("Filter ") + COLUMN_TITLES[i];
if (clearableInput("##filter", &filters_[i], placeholder.c_str())) {
std::map<int, std::string> filters;
for (int c = 0; c < MessageList::COLUMN_COUNT; ++c) {
if (!filters_[c].empty()) filters[c] = filters_[c];
}
list_.setFilters(filters);
}
ImGui::PopID();
}
}
void MessagesWidget::drawRow(int row) {
const auto &item = list_.items[row];
const bool selected = row == current_row_;
const bool inactive = !can->isMessageActive(item.id);
const auto &m = can->lastMessage(item.id);
const bool seen = item.id.source != INVALID_SOURCE;
const bool multiple_lines = settings.multiple_lines_hex;
const float row_height = bytesCellSize(seen ? m.dat.size() : 0, multiple_lines).y - ImGui::GetStyle().CellPadding.y * 2;
ImGui::TableNextRow();
ImGui::PushID(row);
bool row_item_submitted = false;
for (int column = 0; column < MessageList::COLUMN_COUNT; ++column) {
if (!ImGui::TableSetColumnIndex(column)) continue;
const ImVec2 pos = ImGui::GetCursorScreenPos();
const float width = ImGui::GetContentRegionAvail().x;
const ImRect rect(pos, ImVec2(pos.x + width, pos.y + row_height));
const bool row_item = !row_item_submitted;
if (row_item) {
row_item_submitted = true;
if (viewSelectable("##row", selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_SelectOnClick, ImVec2(0, row_height))) {
setCurrentRow(row);
}
if (selected && scroll_to_current_) {
const ImGuiWindow *inner = ImGui::GetCurrentTable()->InnerWindow;
const float view_top = inner->InnerClipRect.Min.y + inner->DecoInnerSizeY1;
const float view_bottom = inner->InnerClipRect.Max.y;
if (ImGui::GetItemRectMin().y < view_top) {
ImGui::SetScrollHereY(0.0f);
} else if (ImGui::GetItemRectMax().y > view_bottom) {
ImGui::SetScrollHereY(1.0f);
}
scroll_to_current_ = false;
}
const ImGuiTableColumn &name_col = ImGui::GetCurrentTable()->Columns[MessageList::NAME];
const float mouse_x = ImGui::GetIO().MousePos.x;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && mouse_x >= name_col.MinX && mouse_x < name_col.MaxX) {
auto msg = dbc()->msg(item.id);
ImGui::BeginTooltip();
ImGui::TextUnformatted(item.name.c_str());
if (msg && !msg->comment.empty()) ImGui::TextDisabled("%s", msg->comment.c_str());
ImGui::EndTooltip();
}
}
if (column == MessageList::DATA && seen) {
drawBytesCell(ImGui::GetWindowDrawList(), rect, m.dat, &m.colors, selected, inactive, multiple_lines);
} else {
drawTextCell(ImGui::GetWindowDrawList(), rect, cellText(item, column), selected, inactive);
}
if (!row_item) ImGui::Dummy(ImVec2(width, row_height));
}
ImGui::PopID();
}