#include "tools/cabana/ui/widgets/detailwidget.h" #include #include #include #include #include #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(); signal_view_ = std::make_unique(charts); history_log_ = std::make_unique(); 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 *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> &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> DetailWidget::serializeMessageIds() const { std::vector 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& 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 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 *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(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> DetailWidget::helpRects() const { std::vector> 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(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(); }