mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-09-18 10:23:43 +08:00
cabana: make Charts a native dockable panel (#38883)
* cabana: make charts a native dockable panel * cabana: remove redundant charts docking button
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<std::string> serializeChartIds() const;
|
||||
@@ -77,9 +78,7 @@ public:
|
||||
|
||||
void setColumnCount(int n);
|
||||
void removeAll();
|
||||
void setIsDocked(bool dock);
|
||||
|
||||
Observable<> toggleChartsDocking;
|
||||
Observable<> seriesChanged;
|
||||
Observable<double> 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_;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -38,9 +38,9 @@ constexpr const char *CHARTS_WINDOW = "Charts###ChartsWindow";
|
||||
MainWindow::MainWindow(GLFWwindow *window, std::unique_ptr<AbstractStream> 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<ChartsWidget>();
|
||||
center_widget_.setChartsWidget(charts_widget_.get());
|
||||
video_widget_ = std::make_unique<VideoWidget>();
|
||||
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);
|
||||
|
||||
@@ -26,7 +26,6 @@ public:
|
||||
MainWindow(GLFWwindow *window, std::unique_ptr<AbstractStream> 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<std::unique_ptr<ToolDialog>> tool_dialogs_;
|
||||
bool closing_ = false;
|
||||
bool exited_ = false;
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user