#include "tools/cabana/ui/widgets/messageswidget.h" #include #include #include #include #include #include #include #include #include #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::min(); unsigned int max = std::numeric_limits::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 *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 &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 &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 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 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 *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"( Message View
Byte color
constant changing
increasing
decreasing
Shortcuts
Horizontal Scrolling:  shift+wheel  )"; } 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(), [](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(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 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(); }