From 9ec262bbfd09c7cd12773cabab0bb6d933b4d13e Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 7 Oct 2022 04:57:11 +0800 Subject: [PATCH] cabana: Docking and undocking charts (#25983) * floating dock charts * more button * setMinimumSize * move reset zoom button to title bar * show chart count * cleanup * reduce flicker * dont update linemarker if pos not changed * cleanup * remove blank line * always show dock/undock button --- tools/cabana/chartswidget.cc | 125 ++++++++++++++++++++++++++++------- tools/cabana/chartswidget.h | 34 +++++++--- tools/cabana/mainwin.cc | 40 ++++++++--- tools/cabana/mainwin.h | 7 +- tools/cabana/signaledit.cc | 2 - tools/cabana/videowidget.cc | 1 + 6 files changed, 161 insertions(+), 48 deletions(-) diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index f964e5606a..3e1a8b8410 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -27,30 +26,105 @@ int64_t get_raw_value(uint8_t *data, size_t data_size, const Signal &sig) { return ret; } +// ChartsWidget + ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) { - main_layout = new QVBoxLayout(this); + QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); - connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart); - connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart); - connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart); + + // title bar + title_bar = new QWidget(this); + QHBoxLayout *title_layout = new QHBoxLayout(title_bar); + title_label = new QLabel(tr("Charts")); + + title_layout->addWidget(title_label); + title_layout->addStretch(); + + reset_zoom_btn = new QPushButton("⟲", this); + reset_zoom_btn->setVisible(false); + reset_zoom_btn->setFixedSize(30, 30); + reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)")); + title_layout->addWidget(reset_zoom_btn); + + remove_all_btn = new QPushButton(tr("✖")); + remove_all_btn->setVisible(false); + remove_all_btn->setToolTip(tr("Remove all charts")); + remove_all_btn->setFixedSize(30, 30); + title_layout->addWidget(remove_all_btn); + + dock_btn = new QPushButton(); + dock_btn->setFixedSize(30, 30); + updateDockButton(); + title_layout->addWidget(dock_btn); + + main_layout->addWidget(title_bar, 0, Qt::AlignTop); + + // charts + QWidget *charts_container = new QWidget(this); + QVBoxLayout *charts_main = new QVBoxLayout(charts_container); + charts_layout = new QVBoxLayout(); + charts_main->addLayout(charts_layout); + charts_main->addStretch(); + + QScrollArea *charts_scroll = new QScrollArea(this); + charts_scroll->setWidgetResizable(true); + charts_scroll->setWidget(charts_container); + charts_scroll->setFrameShape(QFrame::NoFrame); + charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + main_layout->addWidget(charts_scroll); + + QObject::connect(parser, &Parser::showPlot, this, &ChartsWidget::addChart); + QObject::connect(parser, &Parser::hidePlot, this, &ChartsWidget::removeChart); + QObject::connect(parser, &Parser::signalRemoved, this, &ChartsWidget::removeChart); + QObject::connect(reset_zoom_btn, &QPushButton::clicked, parser, &Parser::resetRange); + QObject::connect(remove_all_btn, &QPushButton::clicked, this, &ChartsWidget::removeAll); + QObject::connect(dock_btn, &QPushButton::clicked, [=]() { + emit dock(!docking); + docking = !docking; + updateDockButton(); + }); +} + +void ChartsWidget::updateDockButton() { + dock_btn->setText(docking ? "⬈" : "⬋"); + dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); } void ChartsWidget::addChart(const QString &id, const QString &sig_name) { const QString char_name = id + sig_name; if (charts.find(char_name) == charts.end()) { auto chart = new ChartWidget(id, sig_name, this); - main_layout->insertWidget(0, chart); + charts_layout->insertWidget(0, chart); charts[char_name] = chart; } + remove_all_btn->setVisible(true); + reset_zoom_btn->setVisible(true); + title_label->setText(tr("Charts (%1)").arg(charts.size())); } void ChartsWidget::removeChart(const QString &id, const QString &sig_name) { if (auto it = charts.find(id + sig_name); it != charts.end()) { it->second->deleteLater(); charts.erase(it); + if (charts.empty()) { + remove_all_btn->setVisible(false); + reset_zoom_btn->setVisible(false); + } } + title_label->setText(tr("Charts (%1)").arg(charts.size())); +} + +void ChartsWidget::removeAll() { + for (auto [_, chart] : charts) + chart->deleteLater(); + charts.clear(); + remove_all_btn->setVisible(false); + reset_zoom_btn->setVisible(false); } +// ChartWidget + ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *parent) : id(id), sig_name(sig_name), QWidget(parent) { QStackedLayout *stacked = new QStackedLayout(this); stacked->setStackingMode(QStackedLayout::StackAll); @@ -64,17 +138,13 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa header->setStyleSheet("background-color:white"); QHBoxLayout *header_layout = new QHBoxLayout(header); header_layout->setContentsMargins(11, 11, 11, 0); - auto title = new QLabel(tr("%1 %2").arg(parser->getMsg(id)->name.c_str()).arg(id)); + QLabel *title = new QLabel(tr("%1 %2").arg(parser->getMsg(id)->name.c_str()).arg(id)); header_layout->addWidget(title); header_layout->addStretch(); - zoom_label = new QLabel("", this); - header_layout->addWidget(zoom_label); - QPushButton *zoom_in = new QPushButton("↺", this); - zoom_in->setToolTip(tr("reset zoom")); - QObject::connect(zoom_in, &QPushButton::clicked, []() { parser->resetRange(); }); - header_layout->addWidget(zoom_in); QPushButton *remove_btn = new QPushButton("✖", this); + remove_btn->setFixedSize(30, 30); + remove_btn->setToolTip(tr("Remove chart")); QObject::connect(remove_btn, &QPushButton::clicked, [=]() { emit parser->hidePlot(id, sig_name); }); @@ -108,7 +178,7 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa chart_layout->addStretch(); stacked->addWidget(chart_widget); - line_marker = new LineMarker(chart, this); + line_marker = new LineMarker(this); stacked->addWidget(line_marker); line_marker->setAttribute(Qt::WA_TransparentForMouseEvents, true); line_marker->raise(); @@ -122,7 +192,15 @@ ChartWidget::ChartWidget(const QString &id, const QString &sig_name, QWidget *pa } void ChartWidget::updateState() { - line_marker->update(); + auto chart = chart_view->chart(); + auto axis_x = dynamic_cast(chart->axisX()); + if (axis_x->max() <= axis_x->min()) return; + + int x = chart->plotArea().left() + chart->plotArea().width() * (parser->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); + if (line_marker_x != x) { + line_marker->setX(x); + line_marker_x = x; + } } void ChartWidget::updateSeries() { @@ -182,16 +260,15 @@ void ChartWidget::rangeChanged(qreal min, qreal max) { chart_view->chart()->axisY()->setRange(min_y * 0.95, max_y * 1.05); } -LineMarker::LineMarker(QChart *chart, QWidget *parent) : chart(chart), QWidget(parent) {} +// LineMarker -void LineMarker::paintEvent(QPaintEvent *event) { - auto axis_x = dynamic_cast(chart->axisX()); - if (axis_x->max() <= axis_x->min()) return; +void LineMarker::setX(double x) { + x_pos = x; + update(); +} - double x = chart->plotArea().left() + chart->plotArea().width() * (parser->currentSec() - axis_x->min()) / (axis_x->max() - axis_x->min()); +void LineMarker::paintEvent(QPaintEvent *event) { QPainter p(this); - QPen pen = QPen(Qt::black); - pen.setWidth(2); - p.setPen(pen); - p.drawLine(QPointF{x, 50.}, QPointF{x, (qreal)height() - 11}); + p.setPen(QPen(Qt::black, 2)); + p.drawLine(QPointF{x_pos, 50.}, QPointF{x_pos, (qreal)height() - 11}); } diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index 1be5fdeecb..0413d65e09 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -16,11 +17,12 @@ class LineMarker : public QWidget { Q_OBJECT public: - LineMarker(QChart *chart, QWidget *parent); - void paintEvent(QPaintEvent *event) override; + LineMarker(QWidget *parent) : QWidget(parent) {} + void setX(double x); private: - QChart *chart; + void paintEvent(QPaintEvent *event) override; + double x_pos = 0.0; }; class ChartWidget : public QWidget { @@ -30,7 +32,7 @@ public: ChartWidget(const QString &id, const QString &sig_name, QWidget *parent); inline QChart *chart() const { return chart_view->chart(); } -protected: +private: void updateState(); void addData(const CanData &can_data, const Signal &sig); void updateSeries(); @@ -38,9 +40,9 @@ protected: QString id; QString sig_name; - QLabel *zoom_label; QChartView *chart_view = nullptr; LineMarker *line_marker = nullptr; + double line_marker_x = 0.0; QList vals; }; @@ -49,14 +51,26 @@ class ChartsWidget : public QWidget { public: ChartsWidget(QWidget *parent = nullptr); - inline bool hasChart(const QString &id, const QString &sig_name) { - return charts.find(id+sig_name) != charts.end(); - } void addChart(const QString &id, const QString &sig_name); void removeChart(const QString &id, const QString &sig_name); + void removeAll(); + inline bool hasChart(const QString &id, const QString &sig_name) { + return charts.find(id + sig_name) != charts.end(); + } + +signals: + void dock(bool floating); + +private: void updateState(); + void updateDockButton(); -protected: - QVBoxLayout *main_layout; + QWidget *title_bar; + QLabel *title_label; + bool docking = true; + QPushButton *dock_btn; + QPushButton *reset_zoom_btn; + QPushButton *remove_all_btn; + QVBoxLayout *charts_layout; std::map charts; }; diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 079f592362..49e6cd2cca 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -1,6 +1,7 @@ #include "tools/cabana/mainwin.h" #include +#include #include MainWindow::MainWindow() : QWidget() { @@ -16,23 +17,42 @@ MainWindow::MainWindow() : QWidget() { detail_widget->setFixedWidth(600); h_layout->addWidget(detail_widget); - // right widget + // right widgets QWidget *right_container = new QWidget(this); right_container->setFixedWidth(640); - QVBoxLayout *r_layout = new QVBoxLayout(right_container); + r_layout = new QVBoxLayout(right_container); + video_widget = new VideoWidget(this); - r_layout->addWidget(video_widget); + r_layout->addWidget(video_widget, 0, Qt::AlignTop); charts_widget = new ChartsWidget(this); - QScrollArea *scroll = new QScrollArea(this); - scroll->setWidget(charts_widget); - scroll->setWidgetResizable(true); - scroll->setFrameShape(QFrame::NoFrame); - scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - scroll->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); - r_layout->addWidget(scroll); + r_layout->addWidget(charts_widget); h_layout->addWidget(right_container); QObject::connect(messages_widget, &MessagesWidget::msgChanged, detail_widget, &DetailWidget::setMsg); + QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts); +} + +void MainWindow::dockCharts(bool dock) { + charts_widget->setUpdatesEnabled(false); + if (dock && floating_window) { + r_layout->addWidget(charts_widget); + floating_window->deleteLater(); + floating_window = nullptr; + } else if (!dock && !floating_window) { + floating_window = new QWidget(nullptr); + floating_window->setLayout(new QVBoxLayout()); + floating_window->layout()->addWidget(charts_widget); + floating_window->setWindowFlags(Qt::WindowTitleHint | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint); + floating_window->setMinimumSize(QGuiApplication::primaryScreen()->size() / 2); + floating_window->showMaximized(); + } + charts_widget->setUpdatesEnabled(true); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + if (floating_window) + floating_window->deleteLater(); + QWidget::closeEvent(event); } diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 82ecceb02b..bcd15e4d8e 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "tools/cabana/chartswidget.h" #include "tools/cabana/detailwidget.h" #include "tools/cabana/messageswidget.h" @@ -13,10 +11,15 @@ class MainWindow : public QWidget { public: MainWindow(); + void dockCharts(bool dock); protected: + void closeEvent(QCloseEvent *event) override; + VideoWidget *video_widget; MessagesWidget *messages_widget; DetailWidget *detail_widget; ChartsWidget *charts_widget; + QWidget *floating_window = nullptr; + QVBoxLayout *r_layout; }; diff --git a/tools/cabana/signaledit.cc b/tools/cabana/signaledit.cc index e233b7b3c2..c214adab09 100644 --- a/tools/cabana/signaledit.cc +++ b/tools/cabana/signaledit.cc @@ -91,9 +91,7 @@ SignalEdit::SignalEdit(const QString &id, const Signal &sig, const QString &colo title_layout->addStretch(); plot_btn = new QPushButton("📈"); - plot_btn->setStyleSheet("font-size:16px"); plot_btn->setToolTip(tr("Show Plot")); - plot_btn->setContentsMargins(5, 5, 5, 5); plot_btn->setFixedSize(30, 30); QObject::connect(plot_btn, &QPushButton::clicked, [=]() { emit parser->showPlot(id, name_); }); title_layout->addWidget(plot_btn); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index dbf988d8f2..c8e1ad680f 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -64,6 +64,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) { } main_layout->addLayout(control_layout); + setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); QObject::connect(parser, &Parser::rangeChanged, this, &VideoWidget::rangeChanged); QObject::connect(parser, &Parser::updated, this, &VideoWidget::updateState);