#include "tools/cabana/ui/widgets/historylog.h" #include #include #include #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 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::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 msgs; std::vector 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 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 cmp = nullptr; switch (comp_box_) { case 0: cmp = std::greater{}; break; case 1: cmp = std::equal_to{}; break; case 2: cmp = [](double l, double r) { return l != r; }; break; case 3: cmp = std::less{}; 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 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); }