diff --git a/openpilot/tools/cabana/ui/chart/chartswidget.cc b/openpilot/tools/cabana/ui/chart/chartswidget.cc index 8052931885..12bf656a68 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.cc +++ b/openpilot/tools/cabana/ui/chart/chartswidget.cc @@ -56,7 +56,6 @@ ChartsWidget::ChartsWidget() { if (index != -1) updateLayout(); })); - setIsDocked(true); newTab(); } @@ -153,11 +152,6 @@ void ChartsWidget::setMaxChartRange(int value) { updateState(); } -void ChartsWidget::setIsDocked(bool docked) { - is_docked_ = docked; - if (!docked) float_window_init_ = true; -} - void ChartsWidget::drawToolBar() { float slider_width = 150.0f; const bool is_zoomed = can->timeRange().has_value(); @@ -170,11 +164,6 @@ void ChartsWidget::drawToolBar() { items.push_back({iconButtonWidth(), [this]() { if (iconButton("new_tab_btn", icon::WINDOW_PLUS, "New Tab")) newTab(); }}); - const std::string title_label = "Charts: " + std::to_string(charts_.size()); - items.push_back({ImGui::CalcTextSize(title_label.c_str()).x, [&title_label]() { - ImGui::AlignTextToFramePadding(); - ImGui::TextUnformatted(title_label.c_str()); - }}); const int type_count = (int)std::size(SERIES_TYPE_NAMES); const std::string chart_type_text = std::string("Type: ") + SERIES_TYPE_NAMES[std::clamp(settings.chart_series_type, 0, type_count - 1)]; @@ -247,9 +236,6 @@ void ChartsWidget::drawToolBar() { }}); } items.push_back(toolbarAction("remove_all_btn", icon::TRASH, "Remove all charts", [this]() { removeAll(); }, !charts_.empty())); - const char *dock_btn_icon = is_docked_ ? icon::BOX_ARROW_UP_RIGHT : icon::BOX_ARROW_IN_DOWN_LEFT; - const char *dock_label = is_docked_ ? "Float the charts window" : "Dock the charts window"; - items.push_back(toolbarAction("dock_btn", dock_btn_icon, dock_label, [this]() { toggleChartsDocking(); })); // the slider shrinks first, the buttons stay pinned to the right edge if (slider_index != (size_t)-1) { @@ -575,15 +561,6 @@ void ChartsWidget::handleEvents() { void ChartsWidget::draw() { deleted_charts_.clear(); - // the floating window is a top level window sized to its contents: keep it inside the main viewport so its - // toolbar stays reachable, then let the user resize it - if (float_window_init_ && !is_docked_) { - float_window_init_ = false; - const ImGuiViewport *viewport = ImGui::GetMainViewport(); - const ImVec2 size(viewport->WorkSize.x * 0.6f, viewport->WorkSize.y * 0.6f); - ImGui::SetWindowSize(size); - ImGui::SetWindowPos(viewport->WorkPos + (viewport->WorkSize - size) * 0.5f); - } ImGui::PushID(this); if (auto_scroll_timer_active_ && ImGui::GetTime() >= auto_scroll_timer_next_) { auto_scroll_timer_next_ = ImGui::GetTime() + 0.05; diff --git a/openpilot/tools/cabana/ui/chart/chartswidget.h b/openpilot/tools/cabana/ui/chart/chartswidget.h index c8c1ef7e13..16e1700631 100644 --- a/openpilot/tools/cabana/ui/chart/chartswidget.h +++ b/openpilot/tools/cabana/ui/chart/chartswidget.h @@ -68,7 +68,8 @@ class ChartsWidget { public: ChartsWidget(); ~ChartsWidget(); // out of line: the header users only see a forward declared ChartView - void draw(); // content only; MainWindow wraps it in a child region or the floating window + void draw(); // content only; MainWindow owns the dockable panel + size_t chartCount() const { return charts_.size(); } void showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge); inline bool hasSignal(const MessageId &id, const cabana::Signal *sig) { return findChart(id, sig) != nullptr; } std::vector serializeChartIds() const; @@ -77,9 +78,7 @@ public: void setColumnCount(int n); void removeAll(); - void setIsDocked(bool dock); - Observable<> toggleChartsDocking; Observable<> seriesChanged; Observable showTip; @@ -116,8 +115,6 @@ private: void drawDragPreview(); LogSlider range_slider_{1000}; - bool is_docked_ = true; - bool float_window_init_ = false; // the floating window geometry is set once, right after undocking UndoStack zoom_undo_stack_; diff --git a/openpilot/tools/cabana/ui/inistate.cc b/openpilot/tools/cabana/ui/inistate.cc index 2243690600..4dffec494f 100644 --- a/openpilot/tools/cabana/ui/inistate.cc +++ b/openpilot/tools/cabana/ui/inistate.cc @@ -39,6 +39,8 @@ void readLine(ImGuiContext *, ImGuiSettingsHandler *, void *entry, const char *l state->video_splitter_ratio = ratio; } else if (sscanf(line, "MessagesVisible=%d", &flag) == 1) { state->messages_visible = flag != 0; + } else if (sscanf(line, "ChartsVisible=%d", &flag) == 1) { + state->charts_visible = flag != 0; } else if (sscanf(line, "VideoVisible=%d", &flag) == 1) { state->video_visible = flag != 0; } @@ -54,6 +56,7 @@ void writeAll(ImGuiContext *, ImGuiSettingsHandler *handler, ImGuiTextBuffer *bu buf->appendf("VideoSplitterRatio=%.4f\n", main_window.video_splitter_ratio); buf->appendf("MessagesVisible=%d\n", main_window.messages_visible ? 1 : 0); buf->appendf("VideoVisible=%d\n", main_window.video_visible ? 1 : 0); + buf->appendf("ChartsVisible=%d\n", main_window.charts_visible ? 1 : 0); buf->append("\n"); } diff --git a/openpilot/tools/cabana/ui/inistate.h b/openpilot/tools/cabana/ui/inistate.h index a460802d24..fd17c7e98e 100644 --- a/openpilot/tools/cabana/ui/inistate.h +++ b/openpilot/tools/cabana/ui/inistate.h @@ -15,6 +15,7 @@ struct MainWindowState { float video_splitter_ratio = -1.0f; // < 0: video at its size hint bool messages_visible = true; bool video_visible = true; + bool charts_visible = true; }; extern MainWindowState main_window; diff --git a/openpilot/tools/cabana/ui/mainwin.cc b/openpilot/tools/cabana/ui/mainwin.cc index 0683116424..777f947804 100644 --- a/openpilot/tools/cabana/ui/mainwin.cc +++ b/openpilot/tools/cabana/ui/mainwin.cc @@ -38,9 +38,9 @@ constexpr const char *CHARTS_WINDOW = "Charts###ChartsWindow"; MainWindow::MainWindow(GLFWwindow *window, std::unique_ptr stream, StreamLoader stream_loader, const std::string &dbc_file) : window_(window) { can = &dummy_; - video_splitter_ratio_ = inistate::main_window.video_splitter_ratio; messages_visible_ = inistate::main_window.messages_visible; video_visible_ = inistate::main_window.video_visible; + charts_visible_ = inistate::main_window.charts_visible; loadFingerprints(); std::error_code ec; for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) { @@ -162,10 +162,10 @@ void MainWindow::drawMenuBar() { ImGui::Separator(); 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_); + dropdown::Item("Charts", nullptr, &charts_visible_); ImGui::Separator(); if (dropdown::Item("Reset Window Layout")) { - messages_visible_ = video_visible_ = true; - video_splitter_ratio_ = -1.0f; + messages_visible_ = video_visible_ = charts_visible_ = true; reset_layout_ = true; } dropdown::EndMenu(); @@ -192,7 +192,6 @@ void MainWindow::createDockWidgets() { charts_widget_ = std::make_unique(); center_widget_.setChartsWidget(charts_widget_.get()); video_widget_ = std::make_unique(); - widget_connections_.push_back(charts_widget_->toggleChartsDocking.connect([this]() { toggleChartsDocking(); })); } void MainWindow::showStatusMessage(const std::string &msg, int timeout_ms) { @@ -578,11 +577,6 @@ void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool succe } } -void MainWindow::toggleChartsDocking() { - charts_floating_ = !charts_floating_; - charts_widget_->setIsDocked(!charts_floating_); -} - void MainWindow::close() { if (closing_) return; closing_ = true; @@ -604,9 +598,9 @@ void MainWindow::finishClose() { glfwGetWindowSize(window_, &state.size[0], &state.size[1]); } state.has_geometry = state.size[0] > 0 && state.size[1] > 0; - state.video_splitter_ratio = video_splitter_ratio_; state.messages_visible = messages_visible_; state.video_visible = video_visible_; + state.charts_visible = charts_visible_; settings.ui_state = inistate::save(); saveSessionState(); @@ -787,15 +781,18 @@ void MainWindow::drawDockspace() { ImGui::SetCursorPosY(ImGui::GetCursorPosY() + top_gap); const ImVec2 dock_size(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - status_height); const ImGuiID dock_id = ImGui::GetID("cabana_dockspace"); - if (reset_layout_ || ImGui::DockBuilderGetNode(dock_id) == nullptr) { - // messages left, video (with charts) right, center widget in the middle + if (reset_layout_ || ImGui::DockBuilderGetNode(dock_id) == nullptr || + (!ImGui::FindWindowByName(CHARTS_WINDOW) && !ImGui::FindWindowSettingsByID(ImHashStr(CHARTS_WINDOW)))) { + // Messages left, route above charts on the right, details in the middle. ImGui::DockBuilderRemoveNode(dock_id); ImGui::DockBuilderAddNode(dock_id, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodePos(dock_id, ImGui::GetCursorScreenPos()); ImGui::DockBuilderSetNodeSize(dock_id, dock_size); - ImGuiID center = dock_id, left = 0, right = 0; + ImGuiID center = dock_id, left = 0, right = 0, charts = 0; ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.28f, &left, ¢er); ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.4f, &right, ¢er); + ImGui::DockBuilderSplitNode(right, ImGuiDir_Down, 0.55f, &charts, &right); + ImGui::DockBuilderDockWindow(CHARTS_WINDOW, charts); ImGui::DockBuilderDockWindow(MESSAGES_PANEL_ID, left); ImGui::DockBuilderDockWindow(VIDEO_PANEL, right); ImGui::DockBuilderDockWindow(CENTER_PANEL, center); @@ -856,66 +853,10 @@ void MainWindow::drawVideoPanel() { if (video_widget_ && !video_open) { video_widget_->setVisible(false); // the dock is collapsed or tabbed behind another one, like hideEvent } else if (video_widget_) { - const ImVec2 avail = ImGui::GetContentRegionAvail(); - const bool live = can->liveStreaming(); - // the bordered child pads its content, so the heights the widget asks for grow by the padding - const float video_padding = ImGui::GetStyle().WindowPadding.y * 2.0f; - // the camera is as wide as the child's content region, not the panel - const float default_h = video_widget_->defaultHeight(avail.x - ImGui::GetStyle().WindowPadding.x * 2.0f) + video_padding; - const float video_hint = video_splitter_ratio_ >= 0.0f ? avail.y * video_splitter_ratio_ : default_h; - float video_h = charts_floating_ ? avail.y : std::clamp(video_hint, 0.0f, avail.y - 1.0f); - if (live) video_h = default_h; // display video at minimum size. - // Collapse panes below half their minimum height to keep partially clipped controls out of view. - bool charts_collapsed = false; - const float splitter_h = ImGui::GetStyle().WindowPadding.x * 2.0f + 2.0f; - if (!charts_floating_ && !live) { - const float min_h = std::min(video_widget_->sizeHintHeight() + video_padding, avail.y - 1.0f); - video_h = video_h < min_h / 2 ? 0.0f : std::max(video_h, min_h); - const float charts_min_h = ImGui::GetFrameHeight() + video_padding + ImGui::GetStyle().ChildBorderSize * 2.0f; - const float charts_h = avail.y - video_h - splitter_h; - if (charts_h < charts_min_h / 2) { - charts_collapsed = true; - video_h = avail.y - splitter_h; - } else if (charts_h < charts_min_h) { - video_h = avail.y - splitter_h - charts_min_h; - } - } - // Replay uses a splitter for the gap; live streams use normal item spacing. - if (video_h > 0.0f) { - ImGui::BeginChild("video", ImVec2(0, video_h), ImGuiChildFlags_Borders); - help_overlay_.add(video_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); - video_widget_->draw(); - ImGui::EndChild(); - // The splitter supplies the pane gap; keep normal spacing inside the video child. - if (!charts_floating_ && !live) ImGui::SetCursorPosY(ImGui::GetCursorPosY() - ImGui::GetStyle().ItemSpacing.y); - } else { - video_widget_->setVisible(false); // the splitter collapsed the video: stop the vipc thread - } - if (!charts_floating_ && !live) { - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); - ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, splitter_h)); - const bool splitter_hovered = ImGui::IsItemHovered() && !live, splitter_active = ImGui::IsItemActive() && !live; - if (splitter_active) { - // the size of the video is the position of the handle inside the splitter - const float top = ImGui::GetWindowPos().y + ImGui::GetCursorStartPos().y; - video_splitter_ratio_ = std::clamp((ImGui::GetMousePos().y - top) / avail.y, 0.0f, 1.0f); - } - if (splitter_hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); - const ImRect splitter(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); - const float line_y = std::floor(splitter.GetCenter().y) - 1.0f; - ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(splitter.Min.x, line_y), ImVec2(splitter.Max.x, line_y + 2.0f), - ImGui::GetColorU32(splitter_active ? ImGuiCol_SeparatorActive : splitter_hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Border)); - ImGui::PopStyleVar(); - } - if (!charts_floating_) { - if (!charts_collapsed) { - // the chart list scrolls in its own child, the container itself never scrolls - ImGui::BeginChild("charts", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); - help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); - charts_widget_->draw(); - ImGui::EndChild(); - } - } + ImGui::BeginChild("video", ImVec2(0, 0), ImGuiChildFlags_Borders); + help_overlay_.add(video_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); + video_widget_->draw(); + ImGui::EndChild(); } ImGui::End(); if (!video_visible_ && floating) video_visible_ = reset_layout_ = true; @@ -952,16 +893,16 @@ void MainWindow::draw() { if (messages_visible_) drawMessagesPanel(); if (video_widget_ && !video_visible_) video_widget_->setVisible(false); if (video_visible_) drawVideoPanel(); - if (charts_widget_ && charts_floating_) { - bool open = true; - ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_Appearing); - setNextWindowFloatsOut(); - if (ImGui::Begin(CHARTS_WINDOW, &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { - help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); - charts_widget_->draw(); + if (charts_visible_) { + const std::string charts_title = "Charts: " + std::to_string(charts_widget_ ? charts_widget_->chartCount() : 0) + "###ChartsWindow"; + setNextPanelClass(); + if (beginPanel(charts_title.c_str(), &charts_visible_, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) { + if (charts_widget_) { + help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect()); + charts_widget_->draw(); + } } ImGui::End(); - if (!open) toggleChartsDocking(); } for (auto it = tool_dialogs_.begin(); it != tool_dialogs_.end();) { it = (*it)->draw() ? it + 1 : tool_dialogs_.erase(it); diff --git a/openpilot/tools/cabana/ui/mainwin.h b/openpilot/tools/cabana/ui/mainwin.h index 72c24a3464..840ca0990a 100644 --- a/openpilot/tools/cabana/ui/mainwin.h +++ b/openpilot/tools/cabana/ui/mainwin.h @@ -26,7 +26,6 @@ public: MainWindow(GLFWwindow *window, std::unique_ptr stream, StreamLoader stream_loader, const std::string &dbc_file); ~MainWindow(); void draw(); - void toggleChartsDocking(); void close(); // remind unsaved changes, save state, exit bool exited() const { return exited_; } void showStatusMessage(const std::string &msg, int timeout_ms = 0); @@ -105,13 +104,12 @@ private: std::string video_dock_title_; bool messages_visible_ = true; bool video_visible_ = true; + bool charts_visible_ = true; bool reset_layout_ = false; bool full_screen_ = false; #ifndef __APPLE__ int windowed_rect_[4] = {0, 0, 1600, 900}; #endif - bool charts_floating_ = false; - float video_splitter_ratio_ = -1.0f; // < 0: the video widget is at its size hint std::vector> tool_dialogs_; bool closing_ = false; bool exited_ = false; diff --git a/openpilot/tools/cabana/ui/widgets/videowidget.cc b/openpilot/tools/cabana/ui/widgets/videowidget.cc index 8b269fa8b2..5c8a9ede3c 100644 --- a/openpilot/tools/cabana/ui/widgets/videowidget.cc +++ b/openpilot/tools/cabana/ui/widgets/videowidget.cc @@ -278,9 +278,9 @@ void VideoWidget::drawCameraWidget() { ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f)); camera_tab_->draw(); - // cam_widget_: minimum height MIN_VIDEO_HEIGHT, takes the space left by the slider and the toolbar + // Reserve the timeline and playback controls even when the native dock is short. const ImVec2 avail = ImGui::GetContentRegionAvail(); - const float cam_height = std::max((float)MIN_VIDEO_HEIGHT, avail.y - SLIDER_HEIGHT - toolbar_height); + const float cam_height = std::max(1.0f, avail.y - SLIDER_HEIGHT - toolbar_height); cam_widget_->draw(ImVec2(avail.x, cam_height), thumbnail_display_time_); if (!slider_->isSliderDown()) slider_->setCurrentSecond(can->currentSec());