From 1aa97338c470bef73c30fdb520f655bdff1f4c21 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sat, 12 Sep 2026 20:56:08 -0700 Subject: [PATCH] Cabana: standardize floating dropdown menus (#38874) * Cabana: standardize floating dropdown menus * Cabana: use uniform items in menus and combo lists * Cabana: remove dropdown test and explanatory comments --- openpilot/tools/cabana/ui/chart/chart.cc | 30 ++--- .../tools/cabana/ui/chart/chartswidget.cc | 10 +- .../tools/cabana/ui/chart/signalselector.cc | 6 +- .../tools/cabana/ui/dialogs/routesdialog.cc | 2 +- .../tools/cabana/ui/dialogs/settingsdialog.cc | 4 +- openpilot/tools/cabana/ui/dropdown.h | 90 +++++++++++++ openpilot/tools/cabana/ui/mainwin.cc | 123 +++++++----------- openpilot/tools/cabana/ui/tools/findsignal.cc | 8 +- .../tools/cabana/ui/tools/findsimilarbits.cc | 2 +- openpilot/tools/cabana/ui/util.cc | 32 ++--- openpilot/tools/cabana/ui/util.h | 12 +- .../tools/cabana/ui/widgets/detailwidget.cc | 10 +- .../tools/cabana/ui/widgets/historylog.cc | 6 +- .../tools/cabana/ui/widgets/messageswidget.cc | 10 +- .../tools/cabana/ui/widgets/signalview.cc | 2 +- .../tools/cabana/ui/widgets/videowidget.cc | 9 +- 16 files changed, 195 insertions(+), 161 deletions(-) create mode 100644 openpilot/tools/cabana/ui/dropdown.h diff --git a/openpilot/tools/cabana/ui/chart/chart.cc b/openpilot/tools/cabana/ui/chart/chart.cc index 41256845c1..060bc91541 100644 --- a/openpilot/tools/cabana/ui/chart/chart.cc +++ b/openpilot/tools/cabana/ui/chart/chart.cc @@ -49,20 +49,14 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par } void ChartView::drawMenuActions() { - // the current series type is marked with a radio bullet on the left - const float indent = ImGui::GetFontSize(); - float label_width = ImGui::CalcTextSize("Manage Signals").x; - for (const char *type : SERIES_TYPE_NAMES) label_width = std::max(label_width, ImGui::CalcTextSize(type).x); for (int i = 0; i < (int)std::size(SERIES_TYPE_NAMES); ++i) { - if (radioMenuItem(SERIES_TYPE_NAMES[i], i == (int)series_type_, indent + label_width + indent)) { + if (dropdown::Item(SERIES_TYPE_NAMES[i], nullptr, i == (int)series_type_)) { setSeriesType((SeriesType)i); } } ImGui::Separator(); - ImGui::Indent(indent); - if (ImGui::MenuItem("Manage Signals")) manageSignals(); - if (ImGui::MenuItem("Split Chart", nullptr, false, sigs_.size() > 1)) charts_widget_->splitChart(this); - ImGui::Unindent(indent); + if (dropdown::Item("Manage Signals")) manageSignals(); + if (dropdown::Item("Split Chart", nullptr, false, sigs_.size() > 1)) charts_widget_->splitChart(this); } // the buttons and their menus are drawn every frame, at the rects updateLayout() placed them at @@ -72,9 +66,9 @@ void ChartView::createToolButtons() { ImGui::SetCursorScreenPos(layout_.manage_btn_rect.Min); if (iconButton("manage_btn", icon::THREE_DOTS_VERTICAL, "")) ImGui::OpenPopup("manage_menu"); - if (ImGui::BeginPopup("manage_menu")) { + if (dropdown::BeginPopup("manage_menu")) { drawMenuActions(); - ImGui::EndPopup(); + dropdown::EndPopup(); } if (close_clicked) charts_widget_->removeChart(this); @@ -331,23 +325,19 @@ void ChartView::drawContextMenu() { ImGui::OpenPopup("context_menu"); } context_menu_id_ = ImGui::GetID("context_menu"); - if (ImGui::BeginPopup("context_menu")) { + if (dropdown::BeginPopup("context_menu")) { drawMenuActions(); - // the menu holds checkable entries, so every entry keeps the same left margin - const float indent = ImGui::GetFontSize(); - ImGui::Indent(indent); ImGui::Separator(); // the zoom entries come from the toolbar, where they are only visible while zoomed if (can->timeRange().has_value()) { const std::string undo_text = std::string(icon::ARROW_COUNTERCLOCKWISE) + " Undo Zoom"; const std::string redo_text = std::string(icon::ARROW_CLOCKWISE) + " Redo Zoom"; - if (ImGui::MenuItem(undo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canUndo())) charts_widget_->zoom_undo_stack_.undo(); - if (ImGui::MenuItem(redo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canRedo())) charts_widget_->zoom_undo_stack_.redo(); + if (dropdown::Item(undo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canUndo())) charts_widget_->zoom_undo_stack_.undo(); + if (dropdown::Item(redo_text.c_str(), nullptr, false, charts_widget_->zoom_undo_stack_.canRedo())) charts_widget_->zoom_undo_stack_.redo(); ImGui::Separator(); } - if (ImGui::MenuItem("Close")) charts_widget_->removeChart(this); - ImGui::Unindent(indent); - ImGui::EndPopup(); + if (dropdown::Item("Close")) charts_widget_->removeChart(this); + dropdown::EndPopup(); } } diff --git a/openpilot/tools/cabana/ui/chart/chartswidget.cc b/openpilot/tools/cabana/ui/chart/chartswidget.cc index 94a5fa81b2..8052931885 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.cc +++ b/openpilot/tools/cabana/ui/chart/chartswidget.cc @@ -43,13 +43,13 @@ ChartsWidget::ChartsWidget() { connections_.push_back(seriesChanged.connect([this]() { updateTabBar(); })); connections_.push_back(tabbar_.tabCloseRequested.connect([this](int index) { removeTab(index); })); connections_.push_back(tabbar_.tabContextMenu.connect([this](int index) { - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Close Other Tabs")) { + if (dropdown::BeginPopupContextItem()) { + if (dropdown::Item("Close Other Tabs")) { tabbar_.moveTab(index, 0); tabbar_.setCurrentIndex(0); while (tabbar_.count() > 1) removeTab(1); } - ImGui::EndPopup(); + dropdown::EndPopup(); } })); connections_.push_back(tabbar_.currentChanged.connect([this](int index) { @@ -180,7 +180,7 @@ void ChartsWidget::drawToolBar() { const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)]; auto chart_type_items = [this]() { for (int i = 0; i < type_count; ++i) { - if (ImGui::MenuItem(SERIES_TYPE_NAMES[i], nullptr, settings.chart_series_type == i)) { + if (dropdown::Item(SERIES_TYPE_NAMES[i], nullptr, settings.chart_series_type == i)) { settings.chart_series_type = i; settingChanged(); } @@ -192,7 +192,7 @@ void ChartsWidget::drawToolBar() { if (columns_action_visible_) { auto column_items = [this]() { for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { - if (ImGui::MenuItem(std::to_string(i + 1).c_str(), nullptr, column_count_ == i + 1)) setColumnCount(i + 1); + if (dropdown::Item(std::to_string(i + 1).c_str(), nullptr, column_count_ == i + 1)) setColumnCount(i + 1); } }; items.push_back(toolbarMenu("columns", columns_action_text, "Columns", column_items)); diff --git a/openpilot/tools/cabana/ui/chart/signalselector.cc b/openpilot/tools/cabana/ui/chart/signalselector.cc index 1f29a99ac7..2ea6d70121 100644 --- a/openpilot/tools/cabana/ui/chart/signalselector.cc +++ b/openpilot/tools/cabana/ui/chart/signalselector.cc @@ -46,7 +46,7 @@ bool SignalSelector::draw() { // a combo popup with a filter box const char *preview = msgs_combo_index_ >= 0 ? msgs_combo_[msgs_combo_index_].text.c_str() : "Select a message..."; ImGui::SetNextItemWidth(column_w); - if (ImGui::BeginCombo("##msgs_combo", preview)) { + if (dropdown::BeginCombo("##msgs_combo", preview)) { if (ImGui::IsWindowAppearing()) { msgs_combo_filter_.clear(); // reopen showing the full list ImGui::SetKeyboardFocusHere(); @@ -55,13 +55,13 @@ bool SignalSelector::draw() { inputText("##msgs_filter", &msgs_combo_filter_, "Select a message..."); for (int i = 0; i < (int)msgs_combo_.size(); ++i) { if (!msgs_combo_filter_.empty() && !utils::containsCI(msgs_combo_[i].text, msgs_combo_filter_)) continue; - if (selectable(msgs_combo_[i].text.c_str(), i == msgs_combo_index_)) { + if (dropdown::Item(msgs_combo_[i].text.c_str(), nullptr, i == msgs_combo_index_)) { msgs_combo_index_ = i; updateAvailableList(i); ImGui::CloseCurrentPopup(); } } - ImGui::EndCombo(); + dropdown::EndCombo(); } bool add_dbl = false; drawList("##available_list", available_list_, &available_row_, false, &add_dbl, ImVec2(column_w, lists_h)); diff --git a/openpilot/tools/cabana/ui/dialogs/routesdialog.cc b/openpilot/tools/cabana/ui/dialogs/routesdialog.cc index 9e21464985..f0a5eb0b9e 100644 --- a/openpilot/tools/cabana/ui/dialogs/routesdialog.cc +++ b/openpilot/tools/cabana/ui/dialogs/routesdialog.cc @@ -95,7 +95,7 @@ void RoutesDialog::draw() { ImGui::EndDisabled(); } ImGui::SetNextItemWidth(-1.0f); - if (ImGui::Combo("##period", &s_.period_index, PERIOD_NAMES, IM_ARRAYSIZE(PERIOD_NAMES))) fetchRoutes(); + if (dropdown::Combo("##period", &s_.period_index, PERIOD_NAMES, IM_ARRAYSIZE(PERIOD_NAMES))) fetchRoutes(); bool accepted = false, rejected = false; const float footer = ImGui::GetFrameHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y; diff --git a/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc b/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc index 96f10cff1a..c8dd4fb665 100644 --- a/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc +++ b/openpilot/tools/cabana/ui/dialogs/settingsdialog.cc @@ -60,14 +60,14 @@ void SettingsDialog::draw() { static const char *themes[] = {"Light", "Dark"}; formRow(THEME, label_width); int theme_index = theme_ - LIGHT_THEME; - if (ImGui::Combo("##theme", &theme_index, themes, IM_ARRAYSIZE(themes))) theme_ = theme_index + LIGHT_THEME; + if (dropdown::Combo("##theme", &theme_index, themes, IM_ARRAYSIZE(themes))) theme_ = theme_index + LIGHT_THEME; formRow(CACHED_MINUTES, label_width); settingInputInt("cached_minutes", &cached_minutes_, 1, 10, MIN_CACHE_MINUTES, MAX_CACHE_MINUTES); ImGui::SeparatorText("New Signal Settings"); static const char *directions[] = {"MSB First", "LSB First", "Always Little Endian", "Always Big Endian"}; formRow(DRAG_DIRECTION, label_width); - ImGui::Combo("##drag_direction", &drag_direction_, directions, IM_ARRAYSIZE(directions)); + dropdown::Combo("##drag_direction", &drag_direction_, directions, IM_ARRAYSIZE(directions)); ImGui::SeparatorText("Chart"); formRow(CHART_HEIGHT, label_width); diff --git a/openpilot/tools/cabana/ui/dropdown.h b/openpilot/tools/cabana/ui/dropdown.h new file mode 100644 index 0000000000..dc46d55252 --- /dev/null +++ b/openpilot/tools/cabana/ui/dropdown.h @@ -0,0 +1,90 @@ +#pragma once + +#include +#include + +#include "tools/cabana/ui/theme.h" + +namespace dropdown { +constexpr float PADDING_X = 9.0f; +constexpr float PADDING_Y = 6.0f; +constexpr float SPACING_X = 10.0f; +constexpr float SPACING_Y = 8.0f; +constexpr float ROUNDING = 6.0f; +constexpr float BORDER = 1.0f; +constexpr int WINDOW_STYLE_VARS = 3; + +inline void pushStyle() { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(PADDING_X, PADDING_Y)); + ImGui::PushStyleVar(ImGuiStyleVar_PopupRounding, ROUNDING); + ImGui::PushStyleVar(ImGuiStyleVar_PopupBorderSize, BORDER); +} + +inline bool finishBegin(bool open) { + if (open) ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(SPACING_X, SPACING_Y)); + else ImGui::PopStyleVar(WINDOW_STYLE_VARS); + return open; +} +inline bool BeginPopup(const char *id, ImGuiWindowFlags flags = 0) { + pushStyle(); + return finishBegin(ImGui::BeginPopup(id, flags)); +} +inline bool BeginPopupContextItem(const char *id = nullptr, ImGuiPopupFlags flags = ImGuiPopupFlags_MouseButtonRight) { + pushStyle(); + return finishBegin(ImGui::BeginPopupContextItem(id, flags)); +} +inline void EndPopup() { + ImGui::PopStyleVar(); + ImGui::EndPopup(); + ImGui::PopStyleVar(WINDOW_STYLE_VARS); +} +inline bool BeginMenu(const char *label, bool enabled = true) { + pushStyle(); + return finishBegin(ImGui::BeginMenu(label, enabled)); +} +inline void EndMenu() { + ImGui::PopStyleVar(); + ImGui::EndMenu(); + ImGui::PopStyleVar(WINDOW_STYLE_VARS); +} +inline bool BeginCombo(const char *label, const char *preview, ImGuiComboFlags flags = 0) { + pushStyle(); + return finishBegin(ImGui::BeginCombo(label, preview, flags)); +} +inline void EndCombo() { + ImGui::PopStyleVar(); + ImGui::EndCombo(); + ImGui::PopStyleVar(WINDOW_STYLE_VARS); +} + +inline bool Item(const char *label, const char *shortcut = nullptr, bool selected = false, bool enabled = true) { + return ImGui::MenuItem(label, shortcut, selected, enabled); +} +inline bool Item(const char *label, const char *shortcut, bool *selected, bool enabled = true) { + if (!Item(label, shortcut, selected && *selected, enabled)) return false; + if (selected) *selected = !*selected; + return true; +} + +inline bool Combo(const char *label, int *index, const char *const items[], int count) { + bool changed = false; + if (BeginCombo(label, *index >= 0 && *index < count ? items[*index] : "")) { + for (int i = 0; i < count; ++i) { + ImGui::PushID(i); + if (Item(items[i], nullptr, i == *index) && i != *index) { + *index = i; + changed = true; + } + if (i == *index) ImGui::SetItemDefaultFocus(); + ImGui::PopID(); + } + EndCombo(); + } + return changed; +} +inline bool Combo(const char *label, int *index, const char *items) { + std::vector labels; + for (const char *item = items; *item; item += std::strlen(item) + 1) labels.push_back(item); + return Combo(label, index, labels.data(), labels.size()); +} +} // namespace dropdown diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index 343494a03c..0683116424 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -91,70 +91,47 @@ void MainWindow::loadFingerprints() { void MainWindow::drawFileMenu() { const bool has_stream = hasStream(); - if (ImGui::MenuItem("Open Stream...")) selectAndOpenStream(); - if (ImGui::MenuItem("Close Stream", nullptr, false, has_stream)) closeStream(); - if (ImGui::MenuItem("Export to CSV...", nullptr, false, has_stream)) exportToCSV(); + if (dropdown::Item("Open Stream...")) selectAndOpenStream(); + if (dropdown::Item("Close Stream", nullptr, false, has_stream)) closeStream(); + if (dropdown::Item("Export to CSV...", nullptr, false, has_stream)) exportToCSV(); ImGui::Separator(); - if (ImGui::MenuItem("New DBC File", shortcut("N").c_str())) newFile(); - if (ImGui::MenuItem("Open DBC File...", shortcut("O").c_str())) openFile(); + if (dropdown::Item("New DBC File", shortcut("N").c_str())) newFile(); + if (dropdown::Item("Open DBC File...", shortcut("O").c_str())) openFile(); - if (ImGui::BeginMenu("Manage DBC Files", has_stream)) { + if (dropdown::BeginMenu("Manage DBC Files", has_stream)) { drawManageDBCsMenu(); - ImGui::EndMenu(); + dropdown::EndMenu(); } - if (ImGui::BeginMenu("Open Recent")) { + if (dropdown::BeginMenu("Open Recent")) { drawRecentFilesMenu(); - ImGui::EndMenu(); + dropdown::EndMenu(); } ImGui::Separator(); - if (ImGui::BeginMenu("Load DBC from commaai/opendbc")) { + if (dropdown::BeginMenu("Load DBC from commaai/opendbc")) { for (const auto &name : opendbc_names_) { - if (ImGui::MenuItem(name.c_str())) loadDBCFromOpendbc(name); + if (dropdown::Item(name.c_str())) loadDBCFromOpendbc(name); } - ImGui::EndMenu(); + dropdown::EndMenu(); } - if (ImGui::MenuItem("Load DBC from Clipboard")) loadFromClipboard(); + if (dropdown::Item("Load DBC from Clipboard")) loadFromClipboard(); ImGui::Separator(); const int cnt = dbc()->nonEmptyDBCCount(); const std::string save_text = cnt > 1 ? "Save " + std::to_string(cnt) + " DBCs..." : "Save DBC..."; - if (ImGui::MenuItem(save_text.c_str(), shortcut("S").c_str(), false, cnt > 0)) save(); - if (ImGui::MenuItem("Save DBC As...", shortcut("Shift+S").c_str(), false, cnt == 1)) saveAs(); + if (dropdown::Item(save_text.c_str(), shortcut("S").c_str(), false, cnt > 0)) save(); + if (dropdown::Item("Save DBC As...", shortcut("Shift+S").c_str(), false, cnt == 1)) saveAs(); // TODO: Support clipboard for multiple files - if (ImGui::MenuItem("Copy DBC to Clipboard", nullptr, false, cnt == 1)) saveToClipboard(); + if (dropdown::Item("Copy DBC to Clipboard", nullptr, false, cnt == 1)) saveToClipboard(); ImGui::Separator(); - if (ImGui::MenuItem("Settings...")) openSettings(); + if (dropdown::Item("Settings...")) openSettings(); ImGui::Separator(); - if (ImGui::MenuItem("Exit", shortcut("Q").c_str())) close(); + if (dropdown::Item("Exit", shortcut("Q").c_str())) close(); } -namespace { -bool beginTopMenu(const char *label, bool enabled = true) { - ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImGui::GetColorU32(ImGuiCol_Header)); - const bool open = ImGui::BeginMenu(label, enabled); - ImGui::PopStyleColor(); - if (open) { - // Erase the popup's rounded top border so it joins the menu bar's separator. - const ImGuiStyle &style = ImGui::GetStyle(); - const ImGuiWindow *w = ImGui::GetCurrentWindow(); - const float r = style.PopupRounding, b = style.PopupBorderSize; - const ImVec2 min = w->Pos, max(w->Pos.x + w->Size.x, w->Pos.y + w->Size.y); - const ImU32 bg = ImGui::GetColorU32(ImGuiCol_PopupBg), border = ImGui::GetColorU32(ImGuiCol_Border); - ImDrawList *dl = w->DrawList; - dl->PushClipRect(min, max, false); // the window's own clip rect excludes its border - dl->AddRectFilled(min, ImVec2(max.x, min.y + r), bg); - dl->AddRectFilled(ImVec2(min.x, min.y), ImVec2(min.x + b, min.y + r), border); - dl->AddRectFilled(ImVec2(max.x - b, min.y), ImVec2(max.x, min.y + r), border); - dl->PopClipRect(); - } - return open; -} -} // namespace - void MainWindow::drawMenuBar() { // Avoid a double border with the separator drawn below. ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); @@ -166,43 +143,43 @@ void MainWindow::drawMenuBar() { const ImVec2 max(min.x + ImGui::GetWindowWidth(), min.y + ImGui::GetWindowHeight()); ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(min.x, max.y - 1.0f), max, ImGui::GetColorU32(ImGuiCol_Border)); } - if (beginTopMenu("File")) { + if (dropdown::BeginMenu("File")) { drawFileMenu(); - ImGui::EndMenu(); + dropdown::EndMenu(); } - if (beginTopMenu("Edit")) { + if (dropdown::BeginMenu("Edit")) { auto stack = UndoStack::instance(); const std::string undo_text = stack->canUndo() ? "Undo " + stack->undoText() : "Undo"; const std::string redo_text = stack->canRedo() ? "Redo " + stack->redoText() : "Redo"; - if (ImGui::MenuItem(undo_text.c_str(), shortcut("Z").c_str(), false, stack->canUndo())) stack->undo(); - if (ImGui::MenuItem(redo_text.c_str(), shortcut("Shift+Z").c_str(), false, stack->canRedo())) stack->redo(); - ImGui::EndMenu(); + if (dropdown::Item(undo_text.c_str(), shortcut("Z").c_str(), false, stack->canUndo())) stack->undo(); + if (dropdown::Item(redo_text.c_str(), shortcut("Shift+Z").c_str(), false, stack->canRedo())) stack->redo(); + dropdown::EndMenu(); } - if (beginTopMenu("View")) { - if (ImGui::MenuItem("Full Screen", shortcut("F11").c_str())) toggleFullScreen(); + if (dropdown::BeginMenu("View")) { + if (dropdown::Item("Full Screen", shortcut("F11").c_str())) toggleFullScreen(); ImGui::Separator(); - ImGui::MenuItem(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_); - ImGui::MenuItem(video_dock_title_.empty() ? "Video" : video_dock_title_.c_str(), nullptr, &video_visible_); + dropdown::Item(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_); + dropdown::Item(video_dock_title_.empty() ? "Video" : video_dock_title_.c_str(), nullptr, &video_visible_); ImGui::Separator(); - if (ImGui::MenuItem("Reset Window Layout")) { + if (dropdown::Item("Reset Window Layout")) { messages_visible_ = video_visible_ = true; video_splitter_ratio_ = -1.0f; reset_layout_ = true; } - ImGui::EndMenu(); + dropdown::EndMenu(); } - if (beginTopMenu("Tools", hasStream())) { - if (ImGui::MenuItem("Find Similar Bits")) findSimilarBits(); - if (ImGui::MenuItem("Find Signal")) findSignal(); - ImGui::EndMenu(); + if (dropdown::BeginMenu("Tools", hasStream())) { + if (dropdown::Item("Find Similar Bits")) findSimilarBits(); + if (dropdown::Item("Find Signal")) findSignal(); + dropdown::EndMenu(); } - if (beginTopMenu("Help")) { - if (ImGui::MenuItem("Help", "F1")) toggleHelp(); - ImGui::EndMenu(); + if (dropdown::BeginMenu("Help")) { + if (dropdown::Item("Help", "F1")) toggleHelp(); + dropdown::EndMenu(); } ImGui::EndMainMenuBar(); } @@ -527,22 +504,22 @@ void MainWindow::drawManageDBCsMenu() { auto dbc_file = dbc()->findDBCFile(source); const std::string title = "Bus " + std::to_string(source) + " (" + (dbc_file ? dbc_file->name() : "No DBCs loaded") + ")"; ImGui::PushID(source); - if (ImGui::BeginMenu(title.c_str())) { - if (ImGui::MenuItem("New DBC File")) newFile(ss); - if (ImGui::MenuItem("Open DBC File...")) openFile(ss); - if (ImGui::MenuItem("Load DBC from Clipboard")) loadFromClipboard(ss, false); + if (dropdown::BeginMenu(title.c_str())) { + if (dropdown::Item("New DBC File")) newFile(ss); + if (dropdown::Item("Open DBC File...")) openFile(ss); + if (dropdown::Item("Load DBC from Clipboard")) loadFromClipboard(ss, false); // Show sub-menu for each dbc for this source. if (dbc_file) { ImGui::Separator(); - ImGui::MenuItem((dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")").c_str(), nullptr, false, false); - if (ImGui::MenuItem("Save...")) saveFile(dbc_file); - if (ImGui::MenuItem("Save As...")) saveFileAs(dbc_file); - if (ImGui::MenuItem("Copy to Clipboard")) saveFileToClipboard(dbc_file); - if (ImGui::MenuItem("Remove from This Bus...")) closeFile(ss, {}); - if (ImGui::MenuItem("Remove from All Buses...")) closeFile(dbc_file); + dropdown::Item((dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")").c_str(), nullptr, false, false); + if (dropdown::Item("Save...")) saveFile(dbc_file); + if (dropdown::Item("Save As...")) saveFileAs(dbc_file); + if (dropdown::Item("Copy to Clipboard")) saveFileToClipboard(dbc_file); + if (dropdown::Item("Remove from This Bus...")) closeFile(ss, {}); + if (dropdown::Item("Remove from All Buses...")) closeFile(dbc_file); } - ImGui::EndMenu(); + dropdown::EndMenu(); } ImGui::PopID(); } @@ -560,14 +537,14 @@ void MainWindow::updateRecentFiles(const std::string &fn) { void MainWindow::drawRecentFilesMenu() { int num_recent_files = std::min(settings.recent_files.size(), MAX_RECENT_FILES); if (!num_recent_files) { - ImGui::MenuItem("No Recent Files", nullptr, false, false); + dropdown::Item("No Recent Files", nullptr, false, false); return; } for (int i = 0; i < num_recent_files; ++i) { std::string text = std::to_string(i + 1) + " " + std::filesystem::path(settings.recent_files[i]).filename().string(); ImGui::PushID(i); - if (ImGui::MenuItem(text.c_str())) loadFile(settings.recent_files[i]); + if (dropdown::Item(text.c_str())) loadFile(settings.recent_files[i]); ImGui::PopID(); } } diff --git a/openpilot/tools/cabana/ui/tools/findsignal.cc b/openpilot/tools/cabana/ui/tools/findsignal.cc index 734107aa9f..f977aa804a 100644 --- a/openpilot/tools/cabana/ui/tools/findsignal.cc +++ b/openpilot/tools/cabana/ui/tools/findsignal.cc @@ -167,7 +167,7 @@ void FindSignalDlg::drawFindGroup() { ImGui::TextUnformatted("Value"); ImGui::SameLine(); ImGui::SetNextItemWidth(90); - ImGui::Combo("##compare", &compare_, compare_items, compare_count); + dropdown::Combo("##compare", &compare_, compare_items, compare_count); ImGui::SameLine(); ImGui::SetNextItemWidth(80); if (ImGui::IsWindowAppearing()) ImGui::SetKeyboardFocusHere(); @@ -319,12 +319,12 @@ void FindSignalDlg::setInitialSignals() { } void FindSignalDlg::drawContextMenu(int row) { - if (ImGui::BeginPopupContextItem("menu")) { - if (ImGui::MenuItem("Create Signal")) { + if (dropdown::BeginPopupContextItem("menu")) { + if (dropdown::Item("Create Signal")) { auto &s = search_.filtered_signals[row]; UndoStack::instance()->push(new AddSigCommand(s.id, s.sig)); openMessage(s.id); } - ImGui::EndPopup(); + dropdown::EndPopup(); } } diff --git a/openpilot/tools/cabana/ui/tools/findsimilarbits.cc b/openpilot/tools/cabana/ui/tools/findsimilarbits.cc index 1761fd0f51..032308a9a1 100644 --- a/openpilot/tools/cabana/ui/tools/findsimilarbits.cc +++ b/openpilot/tools/cabana/ui/tools/findsimilarbits.cc @@ -64,7 +64,7 @@ bool FindSimilarBitsDlg::draw() { ImGui::TextUnformatted("Equal"); ImGui::SameLine(); ImGui::SetNextItemWidth(60); - ImGui::Combo("##equal", &equal_, "Yes\0No\0"); + dropdown::Combo("##equal", &equal_, "Yes\0No\0"); ImGui::SameLine(); ImGui::TextUnformatted("Minimum Message Count"); ImGui::SameLine(); diff --git a/openpilot/tools/cabana/ui/util.cc b/openpilot/tools/cabana/ui/util.cc index 69eb94980b..36d0dd8498 100644 --- a/openpilot/tools/cabana/ui/util.cc +++ b/openpilot/tools/cabana/ui/util.cc @@ -103,17 +103,17 @@ bool selectable(const char *label, bool selected, ImGuiSelectableFlags flags, co bool comboBox(const char *label, int *index, const std::vector &items) { bool changed = false; const int count = (int)items.size(); - if (ImGui::BeginCombo(label, *index >= 0 && *index < count ? items[*index].c_str() : "")) { + if (dropdown::BeginCombo(label, *index >= 0 && *index < count ? items[*index].c_str() : "")) { for (int i = 0; i < count; ++i) { ImGui::PushID(i); - if (selectable(items[i].c_str(), i == *index) && *index != i) { + if (dropdown::Item(items[i].c_str(), nullptr, i == *index) && *index != i) { *index = i; changed = true; } if (i == *index) ImGui::SetItemDefaultFocus(); ImGui::PopID(); } - ImGui::EndCombo(); + dropdown::EndCombo(); } return changed; } @@ -248,18 +248,6 @@ void disabledItemTooltip(const char *text) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", text); } -bool radioMenuItem(const char *label, bool checked, float width) { - const float indent = ImGui::GetFontSize(); - const ImVec2 pos = ImGui::GetCursorScreenPos(); - const bool clicked = selectable((std::string("##") + label).c_str(), false, ImGuiSelectableFlags_None, - ImVec2(ImMax(width, ImGui::GetContentRegionAvail().x), 0.0f)); - const ImU32 color = ImGui::GetColorU32(ImGuiCol_Text); - ImDrawList *painter = ImGui::GetWindowDrawList(); - if (checked) ImGui::RenderBullet(painter, ImVec2(pos.x + indent / 2, pos.y + ImGui::GetTextLineHeight() / 2), color); - painter->AddText(ImVec2(pos.x + indent, pos.y), color, label); - return clicked; -} - bool PopupOwner::begin(const char *id) { ImGuiWindow *window = ImGui::GetCurrentWindowRead(); // GetCurrentWindow() would mark the fallback window as used if (popup_id == 0) { @@ -510,9 +498,9 @@ ToolbarItem toolbarMenu(const char *id, const std::string &text, const char *lab ToolbarItem item{width, [id, text, items, bold, width]() { const std::string popup_id = std::string(id) + "_menu"; menuButton(id, text, popup_id.c_str(), bold, width); - if (ImGui::BeginPopup(popup_id.c_str())) { + if (dropdown::BeginPopup(popup_id.c_str())) { items(); - ImGui::EndPopup(); + dropdown::EndPopup(); } }, label}; item.submenu = std::move(items); @@ -579,21 +567,21 @@ void drawToolbar(const std::vector &items, size_t spacer_index, flo } // the popup opens inward: its right edge is aligned with the button so it stays inside the window ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMax().x, ImGui::GetItemRectMax().y), ImGuiCond_Always, ImVec2(1, 0)); - if (ImGui::BeginPopup("toolbar_extension_menu")) { + if (dropdown::BeginPopup("toolbar_extension_menu")) { for (size_t i = visible; i < items.size(); ++i) { if (!items[i].in_menu) continue; if (items[i].menu_label.empty()) { items[i].draw(); } else if (items[i].submenu) { - if (ImGui::BeginMenu(items[i].menu_label.c_str(), items[i].enabled)) { + if (dropdown::BeginMenu(items[i].menu_label.c_str(), items[i].enabled)) { items[i].submenu(); - ImGui::EndMenu(); + dropdown::EndMenu(); } - } else if (ImGui::MenuItem(items[i].menu_label.c_str(), nullptr, false, items[i].enabled)) { + } else if (dropdown::Item(items[i].menu_label.c_str(), nullptr, false, items[i].enabled)) { items[i].trigger(); } } - ImGui::EndPopup(); + dropdown::EndPopup(); } } } diff --git a/openpilot/tools/cabana/ui/util.h b/openpilot/tools/cabana/ui/util.h index f431c9a338..3f3cabb71a 100644 --- a/openpilot/tools/cabana/ui/util.h +++ b/openpilot/tools/cabana/ui/util.h @@ -4,7 +4,7 @@ #include #include -#include "tools/cabana/ui/theme.h" +#include "tools/cabana/ui/dropdown.h" #include "tools/cabana/utils/util.h" struct GLFWwindow; @@ -47,17 +47,17 @@ template inline bool comboBox(const char *label, int *index, const T *values, int count) { bool changed = false; const std::string preview = *index >= 0 && *index < count ? std::to_string(values[*index]) : ""; - if (ImGui::BeginCombo(label, preview.c_str())) { + if (dropdown::BeginCombo(label, preview.c_str())) { for (int i = 0; i < count; ++i) { ImGui::PushID(i); - if (selectable(std::to_string(values[i]).c_str(), i == *index) && *index != i) { + if (dropdown::Item(std::to_string(values[i]).c_str(), nullptr, i == *index) && *index != i) { *index = i; changed = true; } if (i == *index) ImGui::SetItemDefaultFocus(); ImGui::PopID(); } - ImGui::EndCombo(); + dropdown::EndCombo(); } return changed; } @@ -90,10 +90,6 @@ float iconTextButtonWidth(const char *icon, const std::string &text); // tooltip for the last item that also shows while the item is disabled void disabledItemTooltip(const char *text); -// exclusive menu action: the bullet sits in the check column and the whole row highlights. `width` is the -// minimum row width, so a narrow popup stays wide enough for every row while the highlight spans the popup. -bool radioMenuItem(const char *label, bool checked, float width = 0.0f); - // A queued modal popup submitted from whichever call site is nested in the top-most modal. draw() is called // both nested in a modal dialog and at the root level; only the level that opened the popup may submit it // (opening at level 0 would make imgui close the parent modal). diff --git a/openpilot/tools/cabana/ui/widgets/detailwidget.cc b/openpilot/tools/cabana/ui/widgets/detailwidget.cc index 39fc68683e..944662b7a1 100644 --- a/openpilot/tools/cabana/ui/widgets/detailwidget.cc +++ b/openpilot/tools/cabana/ui/widgets/detailwidget.cc @@ -86,11 +86,11 @@ void DetailWidget::drawToolBar() { const size_t spacer_index = items.size(); const std::string heatmap_text = "Heatmap: " + (heatmap_live_ ? std::string("Live") : heatmap_all_text_); auto heatmap_items = [this]() { - if (ImGui::MenuItem("Live", nullptr, heatmap_live_) && !heatmap_live_) { + if (dropdown::Item("Live", nullptr, heatmap_live_) && !heatmap_live_) { heatmap_live_ = true; binary_view_->setHeatmapLiveMode(true); } - if (ImGui::MenuItem(heatmap_all_text_.c_str(), nullptr, !heatmap_live_) && heatmap_live_) { + if (dropdown::Item(heatmap_all_text_.c_str(), nullptr, !heatmap_live_) && heatmap_live_) { heatmap_live_ = false; binary_view_->setHeatmapLiveMode(false); } @@ -111,13 +111,13 @@ void DetailWidget::drawToolBar() { } void DetailWidget::showTabBarContextMenu(int index) { - if (ImGui::BeginPopupContextItem()) { - if (ImGui::MenuItem("Close Other Tabs")) { + if (dropdown::BeginPopupContextItem()) { + if (dropdown::Item("Close Other Tabs")) { tabbar_.moveTab(index, 0); tabbar_.setCurrentIndex(0); while (tabbar_.count() > 1) tabbar_.removeTab(1); } - ImGui::EndPopup(); + dropdown::EndPopup(); } } diff --git a/openpilot/tools/cabana/ui/widgets/historylog.cc b/openpilot/tools/cabana/ui/widgets/historylog.cc index 19ce2c9f65..92ed043ea7 100644 --- a/openpilot/tools/cabana/ui/widgets/historylog.cc +++ b/openpilot/tools/cabana/ui/widgets/historylog.cc @@ -142,7 +142,7 @@ void LogsWidget::draw() { 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")) { + if (dropdown::Combo("##display_type", &display_type_cb_, "Signal\0Hex\0")) { hex_mode_ = display_type_cb_; reset(); } @@ -155,10 +155,10 @@ void LogsWidget::draw() { } sig_items += '\0'; ImGui::SetNextItemWidth(SIGNALS_WIDTH); - if (ImGui::Combo("##signals", &signals_cb_, sig_items.c_str())) filterChanged(); + if (dropdown::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(); + if (dropdown::Combo("##comp", &comp_box_, ">\0=\0!=\0<\0")) filterChanged(); ImGui::SameLine(); ImGui::SetNextItemWidth(value_w); if (clearableInput("##value", &value_edit_, "", doubleValidator)) { diff --git a/openpilot/tools/cabana/ui/widgets/messageswidget.cc b/openpilot/tools/cabana/ui/widgets/messageswidget.cc index 52e0687204..a12942aa83 100644 --- a/openpilot/tools/cabana/ui/widgets/messageswidget.cc +++ b/openpilot/tools/cabana/ui/widgets/messageswidget.cc @@ -301,22 +301,22 @@ void MessagesWidget::suppressHighlighted(bool from_suppress_add) { } void MessagesWidget::drawContextMenu() { - if (!ImGui::BeginPopup("menu")) return; + if (!dropdown::BeginPopup("menu")) return; for (int i = 0; i < MessageList::COLUMN_COUNT; ++i) { const int column = display_order_[i]; // can't hide the name column - if (ImGui::MenuItem(COLUMN_TITLES[column], nullptr, !hidden_[column], column > 0)) { + if (dropdown::Item(COLUMN_TITLES[column], nullptr, !hidden_[column], column > 0)) { pending_hidden_.emplace_back(column, !hidden_[column]); } } ImGui::Separator(); - if (ImGui::MenuItem("Multiline Bytes", nullptr, settings.multiple_lines_hex)) { + if (dropdown::Item("Multiline Bytes", nullptr, settings.multiple_lines_hex)) { setMultiLineBytes(!settings.multiple_lines_hex); } - if (ImGui::MenuItem("Show Inactive Messages", nullptr, list_.show_inactive_messages)) { + if (dropdown::Item("Show Inactive Messages", nullptr, list_.show_inactive_messages)) { list_.showInactiveMessages(!list_.show_inactive_messages); } - ImGui::EndPopup(); + dropdown::EndPopup(); } void MessagesWidget::setMultiLineBytes(bool multi) { diff --git a/openpilot/tools/cabana/ui/widgets/signalview.cc b/openpilot/tools/cabana/ui/widgets/signalview.cc index 344bf7a4b3..269df6158c 100644 --- a/openpilot/tools/cabana/ui/widgets/signalview.cc +++ b/openpilot/tools/cabana/ui/widgets/signalview.cc @@ -375,7 +375,7 @@ void SignalView::drawEditor(SignalModel::Item *item) { } const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, ImGui::GetID("##editor")); if (take_focus) ImGui::SetKeyboardFocusHere(); - if (ImGui::Combo("##editor", ¤t, names.data(), names.size())) { + if (dropdown::Combo("##editor", ¤t, names.data(), names.size())) { queueCommit(item, items[current].second); open_item_ = nullptr; // commit and close the editor } diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 7860a95138..8b269fa8b2 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -234,16 +234,9 @@ void VideoWidget::createSpeedDropdown() { } void VideoWidget::drawSpeedMenuItems() { - // every row declares the same width, so the popup is exactly as wide as the widest one and all the - // highlights reach both edges; the label is padded on the right as much as the check column on the left - 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)) { + if (dropdown::Item(speedText(speed).c_str(), nullptr, speed_index_ == i)) { speed_index_ = i; can->setSpeed(speed); speed_text_ = speedText(speed);