From 57bf782872461a3328bbd9121c7bbd207abec2e5 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Wed, 12 Apr 2023 13:30:15 +0800 Subject: [PATCH] cabana: split chart to multiple files (#27871) * git mv * split file * fix includes * split tiplabel out --- tools/cabana/SConscript | 3 +- .../{chartswidget.cc => chart/chart.cc} | 663 +----------------- tools/cabana/chart/chart.h | 106 +++ tools/cabana/chart/chartswidget.cc | 497 +++++++++++++ tools/cabana/chart/chartswidget.h | 125 ++++ tools/cabana/chart/signalselector.cc | 108 +++ tools/cabana/chart/signalselector.h | 30 + tools/cabana/chart/tiplabel.cc | 46 ++ tools/cabana/chart/tiplabel.h | 10 + tools/cabana/chartswidget.h | 249 ------- tools/cabana/detailwidget.h | 2 +- tools/cabana/mainwin.h | 2 +- tools/cabana/signalview.cc | 1 + tools/cabana/signalview.h | 2 +- 14 files changed, 943 insertions(+), 901 deletions(-) rename tools/cabana/{chartswidget.cc => chart/chart.cc} (52%) create mode 100644 tools/cabana/chart/chart.h create mode 100644 tools/cabana/chart/chartswidget.cc create mode 100644 tools/cabana/chart/chartswidget.h create mode 100644 tools/cabana/chart/signalselector.cc create mode 100644 tools/cabana/chart/signalselector.h create mode 100644 tools/cabana/chart/tiplabel.cc create mode 100644 tools/cabana/chart/tiplabel.h delete mode 100644 tools/cabana/chartswidget.h diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index d6c2779ac3..77738afae3 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -29,8 +29,9 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "asset prev_moc_path = cabana_env['QT_MOCHPREFIX'] cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' -cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', +cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'commands.cc', 'messageswidget.cc', 'route.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chart/chart.cc similarity index 52% rename from tools/cabana/chartswidget.cc rename to tools/cabana/chart/chart.cc index 0d34d0ddcc..d01a4882b2 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chart/chart.cc @@ -1,449 +1,19 @@ -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chart.h" #include #include -#include -#include #include -#include -#include #include -#include +#include #include +#include #include -#include #include -#include -#include -#include -#include -#include - -const int MAX_COLUMN_COUNT = 4; -const int CHART_SPACING = 10; -const QString mime_type = "application/x-cabanachartview"; -static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; } - -// ChartsWidget - -ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_timer(this), QFrame(parent) { - setFrameStyle(QFrame::StyledPanel | QFrame::Plain); - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(0, 0, 0, 0); - main_layout->setSpacing(0); - - // toolbar - QToolBar *toolbar = new QToolBar(tr("Charts"), this); - int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); - toolbar->setIconSize({icon_size, icon_size}); - - auto new_plot_btn = new ToolButton("file-plus", tr("New Chart")); - auto new_tab_btn = new ToolButton("window-stack", tr("New Tab")); - toolbar->addWidget(new_plot_btn); - toolbar->addWidget(new_tab_btn); - toolbar->addWidget(title_label = new QLabel()); - title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); - - QMenu *menu = new QMenu(this); - for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { - menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); - } - columns_action = toolbar->addAction(""); - columns_action->setMenu(menu); - qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); - - QLabel *stretch_label = new QLabel(this); - stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - toolbar->addWidget(stretch_label); - - range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); - range_slider = new LogSlider(1000, Qt::Horizontal, this); - range_slider->setMaximumWidth(200); - range_slider->setToolTip(tr("Set the chart range")); - range_slider->setRange(1, settings.max_cached_minutes * 60); - range_slider->setSingleStep(1); - range_slider->setPageStep(60); // 1 min - range_slider_action = toolbar->addWidget(range_slider); - - // zoom controls - zoom_undo_stack = new QUndoStack(this); - toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this)); - undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); - toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this)); - redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); - reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - - toolbar->addWidget(remove_all_btn = new ToolButton("x", tr("Remove all charts"))); - toolbar->addWidget(dock_btn = new ToolButton("")); - main_layout->addWidget(toolbar); - - // tabbar - tabbar = new QTabBar(this); - tabbar->setAutoHide(true); - tabbar->setExpanding(false); - tabbar->setDrawBase(true); - tabbar->setAcceptDrops(true); - tabbar->setChangeCurrentOnDrag(true); - tabbar->setTabsClosable(true); - tabbar->setUsesScrollButtons(true); - main_layout->addWidget(tabbar); - - // charts - charts_container = new ChartsContainer(this); - - charts_scroll = new QScrollArea(this); - charts_scroll->setFrameStyle(QFrame::NoFrame); - charts_scroll->setWidgetResizable(true); - charts_scroll->setWidget(charts_container); - charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - main_layout->addWidget(charts_scroll); - - // init settings - current_theme = settings.theme; - column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); - max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); - display_range = {0, max_chart_range}; - range_slider->setValue(max_chart_range); - updateToolBar(); - - align_timer.setSingleShot(true); - QObject::connect(&align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); - QObject::connect(&auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); - QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); - QObject::connect(can, &AbstractStream::updated, this, &ChartsWidget::updateState); - QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); - QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); - QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); - QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); - QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); - QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); - QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); - QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { - if (index != -1) updateLayout(true); - }); - QObject::connect(dock_btn, &QToolButton::clicked, [this]() { - emit dock(!docking); - docking = !docking; - updateToolBar(); - }); - - newTab(); - setWhatsThis(tr(R"( - Chart view
- - )")); -} - -void ChartsWidget::newTab() { - static int tab_unique_id = 0; - int idx = tabbar->addTab(""); - tabbar->setTabData(idx, tab_unique_id++); - for (int i = 0; i < tabbar->count(); ++i) { - tabbar->setTabText(i, QString("Tab %1").arg(i + 1)); - } - tabbar->setCurrentIndex(idx); -} - -void ChartsWidget::removeTab(int index) { - int id = tabbar->tabData(index).toInt(); - for (auto &c : tab_charts[id]) { - removeChart(c); - } - tab_charts.erase(id); - tabbar->removeTab(index); -} - -void ChartsWidget::eventsMerged() { - QFutureSynchronizer future_synchronizer; - for (auto c : charts) { - future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr)); - } -} - -void ChartsWidget::setZoom(double min, double max) { - zoomed_range = {min, max}; - is_zoomed = zoomed_range != display_range; - updateToolBar(); - updateState(); - emit rangeChanged(min, max, is_zoomed); -} - -void ChartsWidget::zoomReset() { - setZoom(display_range.first, display_range.second); - zoom_undo_stack->clear(); -} - -void ChartsWidget::showValueTip(double sec) { - const QRect visible_rect(-charts_container->pos(), charts_scroll->viewport()->size()); - for (auto c : currentCharts()) { - if (sec >= 0 && visible_rect.contains(QRect(c->mapTo(charts_container, QPoint(0, 0)), c->size()))) { - c->showTip(sec); - } else { - c->hideTip(); - } - } -} - -void ChartsWidget::updateState() { - if (charts.isEmpty()) return; - - const double cur_sec = can->currentSec(); - if (!is_zoomed) { - double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); - if (pos < 0 || pos > 0.8) { - display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); - } - double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->lastEventSecond()); - display_range.first = std::max(0.0, max_sec - max_chart_range); - display_range.second = display_range.first + max_chart_range; - } else if (cur_sec < zoomed_range.first || cur_sec >= zoomed_range.second) { - // loop in zoomed range - can->seekTo(zoomed_range.first); - } - - const auto &range = is_zoomed ? zoomed_range : display_range; - for (auto c : charts) { - c->updatePlot(cur_sec, range.first, range.second); - } -} - -void ChartsWidget::setMaxChartRange(int value) { - max_chart_range = settings.chart_range = range_slider->value(); - updateToolBar(); - updateState(); -} - -void ChartsWidget::updateToolBar() { - title_label->setText(tr("Charts: %1").arg(charts.size())); - columns_action->setText(tr("Column: %1").arg(column_count)); - range_lb->setText(utils::formatSeconds(max_chart_range)); - range_lb_action->setVisible(!is_zoomed); - range_slider_action->setVisible(!is_zoomed); - undo_zoom_action->setVisible(is_zoomed); - redo_zoom_action->setVisible(is_zoomed); - reset_zoom_action->setVisible(is_zoomed); - reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(zoomed_range.first, 0, 'f', 1).arg(zoomed_range.second, 0, 'f', 1) : ""); - remove_all_btn->setEnabled(!charts.isEmpty()); - dock_btn->setIcon(docking ? "arrow-up-right-square" : "arrow-down-left-square"); - dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); -} - -void ChartsWidget::settingChanged() { - if (std::exchange(current_theme, settings.theme) != current_theme) { - undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); - redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - auto theme = settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; - for (auto c : charts) { - c->setTheme(theme); - } - } - range_slider->setRange(1, settings.max_cached_minutes * 60); - for (auto c : charts) { - c->setFixedHeight(settings.chart_height); - c->setSeriesType((SeriesType)settings.chart_series_type); - c->resetChartCache(); - } -} - -ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) { - for (auto c : charts) - if (c->hasSeries(id, sig)) return c; - return nullptr; -} - -ChartView *ChartsWidget::createChart() { - auto chart = new ChartView(is_zoomed ? zoomed_range : display_range, this); - chart->setFixedHeight(settings.chart_height); - chart->setMinimumWidth(CHART_MIN_WIDTH); - chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); - QObject::connect(chart, &ChartView::axisYLabelWidthChanged, &align_timer, qOverload<>(&QTimer::start)); - charts.push_front(chart); - currentCharts().push_front(chart); - updateLayout(true); - updateToolBar(); - return chart; -} - -void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { - ChartView *chart = findChart(id, sig); - if (show && !chart) { - chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart(); - chart->addSeries(id, sig); - } else if (!show && chart) { - chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; }); - } -} - -void ChartsWidget::setColumnCount(int n) { - n = std::clamp(n, 1, MAX_COLUMN_COUNT); - if (column_count != n) { - column_count = settings.chart_column_count = n; - updateToolBar(); - updateLayout(); - } -} - -void ChartsWidget::updateLayout(bool force) { - auto charts_layout = charts_container->charts_layout; - int n = MAX_COLUMN_COUNT; - for (; n > 1; --n) { - if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->spacing()) < charts_layout->geometry().width()) break; - } - - bool show_column_cb = n > 1; - columns_action->setVisible(show_column_cb); - - n = std::min(column_count, n); - auto ¤t_charts = currentCharts(); - if ((current_charts.size() != charts_layout->count() || n != current_column_count) || force) { - current_column_count = n; - charts_container->setUpdatesEnabled(false); - for (auto c : charts) { - c->setVisible(false); - } - for (int i = 0; i < current_charts.size(); ++i) { - charts_layout->addWidget(current_charts[i], i / n, i % n); - current_charts[i]->setVisible(true); - } - charts_container->setUpdatesEnabled(true); - } -} - -void ChartsWidget::startAutoScroll() { - auto_scroll_timer.start(50); -} - -void ChartsWidget::stopAutoScroll() { - auto_scroll_timer.stop(); - auto_scroll_count = 0; -} - -void ChartsWidget::doAutoScroll() { - QScrollBar *scroll = charts_scroll->verticalScrollBar(); - if (auto_scroll_count < scroll->pageStep()) { - ++auto_scroll_count; - } - - int value = scroll->value(); - QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos()); - QRect area = charts_scroll->viewport()->rect(); - - if (pos.y() - area.top() < settings.chart_height / 2) { - scroll->setValue(value - auto_scroll_count); - } else if (area.bottom() - pos.y() < settings.chart_height / 2) { - scroll->setValue(value + auto_scroll_count); - } - bool vertical_unchanged = value == scroll->value(); - if (vertical_unchanged) { - stopAutoScroll(); - } else { - // mouseMoveEvent to updates the drag-selection rectangle - const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos); - const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos); - QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, - Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt); - QApplication::sendEvent(charts_scroll->viewport(), &mm); - } -} - -void ChartsWidget::resizeEvent(QResizeEvent *event) { - QWidget::resizeEvent(event); - updateLayout(); -} - -void ChartsWidget::newChart() { - SeriesSelector dlg(tr("New Chart"), this); - if (dlg.exec() == QDialog::Accepted) { - auto items = dlg.seletedItems(); - if (!items.isEmpty()) { - auto c = createChart(); - for (auto it : items) { - c->addSeries(it->msg_id, it->sig); - } - } - } -} +#include -void ChartsWidget::removeChart(ChartView *chart) { - charts.removeOne(chart); - chart->deleteLater(); - for (auto &[_, list] : tab_charts) { - list.removeOne(chart); - } - updateToolBar(); - updateLayout(true); - alignCharts(); - emit seriesChanged(); -} +#include "tools/cabana/chart/chartswidget.h" -void ChartsWidget::removeAll() { - if (!charts.isEmpty()) { - for (auto c : charts) { - c->deleteLater(); - } - charts.clear(); - tab_charts.clear(); - updateToolBar(); - emit seriesChanged(); - } - while (tabbar->count() > 1) { - tabbar->removeTab(1); - } -} - -void ChartsWidget::alignCharts() { - int plot_left = 0; - for (auto c : charts) { - plot_left = std::max(plot_left, c->y_label_width); - } - plot_left = std::max((plot_left / 10) * 10 + 10, 50); - for (auto c : charts) { - c->updatePlotArea(plot_left); - } -} - -bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { - if (obj != this && event->type() == QEvent::Close) { - emit dock_btn->clicked(); - return true; - } - return false; -} - -bool ChartsWidget::event(QEvent *event) { - bool back_button = false; - switch (event->type()) { - case QEvent::MouseButtonPress: { - QMouseEvent *ev = static_cast(event); - back_button = ev->button() == Qt::BackButton; - break; - } - case QEvent::NativeGesture: { - QNativeGestureEvent *ev = static_cast(event); - back_button = (ev->value() == 180); - break; - } - case QEvent::WindowActivate: - case QEvent::WindowDeactivate: - case QEvent::FocusIn: - case QEvent::FocusOut: - case QEvent::Leave: - showValueTip(-1); - break; - default: - break; - } - - if (back_button) { - zoom_undo_stack->undo(); - return true; - } - return QFrame::event(event); -} - -// ChartView +static inline bool xLessThan(const QPointF &p, float x) { return p.x() < x; } ChartView::ChartView(const std::pair &x_range, ChartsWidget *parent) : charts_widget(parent), tip_label(this), QChartView(nullptr, parent) { series_type = (SeriesType)settings.chart_series_type; @@ -513,6 +83,10 @@ void ChartView::createToolButtons() { }); } +QSize ChartView::sizeHint() const { + return {CHART_MIN_WIDTH, settings.chart_height}; +} + void ChartView::setTheme(QChart::ChartTheme theme) { chart()->setTheme(theme); if (theme == QChart::ChartThemeDark) { @@ -576,7 +150,7 @@ void ChartView::msgUpdated(MessageId id) { } void ChartView::manageSeries() { - SeriesSelector dlg(tr("Mange Chart"), this); + SignalSelector dlg(tr("Mange Chart"), this); for (auto &s : sigs) { dlg.addSelected(s.msg_id, s.sig); } @@ -837,7 +411,7 @@ static QPixmap getDropPixmap(const QPixmap &src) { void ChartView::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && move_icon->sceneBoundingRect().contains(event->pos())) { QMimeData *mimeData = new QMimeData; - mimeData->setData(mime_type, QByteArray::number((qulonglong)this)); + mimeData->setData(CHART_MIME_TYPE, QByteArray::number((qulonglong)this)); QPixmap px = grab().scaledToWidth(CHART_MIN_WIDTH, Qt::SmoothTransformation); QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); @@ -962,14 +536,14 @@ void ChartView::hideTip() { } void ChartView::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { drawDropIndicator(event->source() != this); event->acceptProposedAction(); } } void ChartView::dragMoveEvent(QDragMoveEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { event->setDropAction(event->source() == this ? Qt::MoveAction : Qt::CopyAction); event->accept(); } @@ -977,7 +551,7 @@ void ChartView::dragMoveEvent(QDragMoveEvent *event) { } void ChartView::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { if (event->source() != this) { ChartView *source_chart = (ChartView *)event->source(); for (auto &s : source_chart->sigs) { @@ -1151,210 +725,3 @@ void ChartView::handleMarkerClicked() { updateTitle(); } } - -// SeriesSelector - -SeriesSelector::SeriesSelector(QString title, QWidget *parent) : QDialog(parent) { - setWindowTitle(title); - QGridLayout *main_layout = new QGridLayout(this); - - // left column - main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0); - main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0); - msgs_combo->setEditable(true); - msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); - msgs_combo->setInsertPolicy(QComboBox::NoInsert); - msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); - msgs_combo->completer()->setFilterMode(Qt::MatchContains); - - main_layout->addWidget(available_list = new QListWidget(this), 2, 0); - - // buttons - QVBoxLayout *btn_layout = new QVBoxLayout(); - QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this); - add_btn->setEnabled(false); - QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this); - remove_btn->setEnabled(false); - btn_layout->addStretch(0); - btn_layout->addWidget(add_btn); - btn_layout->addWidget(remove_btn); - btn_layout->addStretch(0); - main_layout->addLayout(btn_layout, 0, 1, 3, 1); - - // right column - main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2); - main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox, 3, 2); - - for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { - if (auto m = dbc()->msg(it.key())) { - msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(it.key().toString()), QVariant::fromValue(it.key())); - } - } - msgs_combo->model()->sort(0); - msgs_combo->setCurrentIndex(-1); - - QObject::connect(msgs_combo, qOverload(&QComboBox::currentIndexChanged), this, &SeriesSelector::updateAvailableList); - QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); }); - QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); }); - QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SeriesSelector::add); - QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SeriesSelector::remove); - QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); }); - QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item); }); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -void SeriesSelector::add(QListWidgetItem *item) { - auto it = (ListItem *)item; - addItemToList(selected_list, it->msg_id, it->sig, true); - delete item; -} - -void SeriesSelector::remove(QListWidgetItem *item) { - auto it = (ListItem *)item; - if (it->msg_id == msgs_combo->currentData().value()) { - addItemToList(available_list, it->msg_id, it->sig); - } - delete item; -} - -void SeriesSelector::updateAvailableList(int index) { - if (index == -1) return; - available_list->clear(); - MessageId msg_id = msgs_combo->itemData(index).value(); - auto selected_items = seletedItems(); - for (auto s : dbc()->msg(msg_id)->getSignals()) { - bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), [=, sig=s](auto it) { return it->msg_id == msg_id && it->sig == sig; }); - if (!is_selected) { - addItemToList(available_list, msg_id, s); - } - } -} - -void SeriesSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { - QString text = QString(" %1").arg(getColor(sig).name(), sig->name); - if (show_msg_name) text += QString(" %0 %1").arg(msgName(id), id.toString()); - - QLabel *label = new QLabel(text); - label->setContentsMargins(5, 0, 5, 0); - auto new_item = new ListItem(id, sig, parent); - new_item->setSizeHint(label->sizeHint()); - parent->setItemWidget(new_item, label); -} - -QList SeriesSelector::seletedItems() { - QList ret; - for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); - return ret; -} - -// ValueTipLabel - -ValueTipLabel::ValueTipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { - setForegroundRole(QPalette::ToolTipText); - setBackgroundRole(QPalette::ToolTipBase); - auto palette = QToolTip::palette(); - if (settings.theme != DARK_THEME) { - palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); - palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush - } - setPalette(palette); - ensurePolished(); - setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this)); - setAttribute(Qt::WA_ShowWithoutActivating); - setTextFormat(Qt::RichText); - setVisible(false); -} - -void ValueTipLabel::showText(const QPoint &pt, const QString &text, int right_edge) { - setText(text); - if (!text.isEmpty()) { - QSize extra(1, 1); - resize(sizeHint() + extra); - QPoint tip_pos(pt.x() + 12, pt.y()); - if (tip_pos.x() + size().width() >= right_edge) { - tip_pos.rx() = pt.x() - size().width() - 12; - } - move(tip_pos); - } - setVisible(!text.isEmpty()); -} - -void ValueTipLabel::paintEvent(QPaintEvent *ev) { - QStylePainter p(this); - QStyleOptionFrame opt; - opt.init(this); - p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); - p.end(); - QLabel::paintEvent(ev); -} - -// ChartsContainer - -ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { - setAcceptDrops(true); - QVBoxLayout *charts_main_layout = new QVBoxLayout(this); - charts_main_layout->setContentsMargins(0, 10, 0, 0); - charts_layout = new QGridLayout(); - charts_layout->setSpacing(CHART_SPACING); - charts_main_layout->addLayout(charts_layout); - charts_main_layout->addStretch(0); -} - -void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { - event->acceptProposedAction(); - drawDropIndicator(event->pos()); - } -} - -void ChartsContainer::dropEvent(QDropEvent *event) { - if (event->mimeData()->hasFormat(mime_type)) { - auto w = getDropAfter(event->pos()); - auto chart = qobject_cast(event->source()); - if (w != chart) { - for (auto &[_, list] : charts_widget->tab_charts) { - list.removeOne(chart); - } - int to = w ? charts_widget->currentCharts().indexOf(w) + 1 : 0; - charts_widget->currentCharts().insert(to, chart); - charts_widget->updateLayout(true); - event->acceptProposedAction(); - } - drawDropIndicator({}); - } -} - -void ChartsContainer::paintEvent(QPaintEvent *ev) { - if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { - QRect r; - if (auto insert_after = getDropAfter(drop_indictor_pos)) { - QRect area = insert_after->geometry(); - r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING); - } else { - r = geometry(); - r.setHeight(CHART_SPACING); - } - - const int margin = (CHART_SPACING - 2) / 2; - QPainterPath path; - path.addPolygon(QPolygonF({r.topLeft(), QPointF(r.left() + CHART_SPACING, r.top() + r.height() / 2), r.bottomLeft()})); - path.addPolygon(QPolygonF({r.topRight(), QPointF(r.right() - CHART_SPACING, r.top() + r.height() / 2), r.bottomRight()})); - - QPainter p(this); - p.setRenderHint(QPainter::Antialiasing); - p.fillPath(path, palette().highlight()); - p.fillRect(r.adjusted(2, margin, -2, -margin), palette().highlight()); - } -} - -ChartView *ChartsContainer::getDropAfter(const QPoint &pos) const { - auto it = std::find_if(charts_widget->currentCharts().crbegin(), charts_widget->currentCharts().crend(), [&pos](auto c) { - auto area = c->geometry(); - return pos.x() >= area.left() && pos.x() <= area.right() && pos.y() >= area.bottom(); - }); - return it == charts_widget->currentCharts().crend() ? nullptr : *it; -} diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h new file mode 100644 index 0000000000..d00ead0f9f --- /dev/null +++ b/tools/cabana/chart/chart.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +using namespace QtCharts; + +#include "tools/cabana/chart/tiplabel.h" +#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/streams/abstractstream.h" + +enum class SeriesType { + Line = 0, + StepLine, + Scatter +}; + +class ChartsWidget; +class ChartView : public QChartView { + Q_OBJECT + +public: + ChartView(const std::pair &x_range, ChartsWidget *parent = nullptr); + void addSeries(const MessageId &msg_id, const cabana::Signal *sig); + bool hasSeries(const MessageId &msg_id, const cabana::Signal *sig) const; + void updateSeries(const cabana::Signal *sig = nullptr); + void updatePlot(double cur, double min, double max); + void setSeriesType(SeriesType type); + void updatePlotArea(int left, bool force = false); + void showTip(double sec); + void hideTip(); + + struct SigItem { + MessageId msg_id; + const cabana::Signal *sig = nullptr; + QXYSeries *series = nullptr; + QVector vals; + QVector step_vals; + uint64_t last_value_mono_time = 0; + QPointF track_pt{}; + SegmentTree segment_tree; + double min = 0; + double max = 0; + }; + +signals: + void axisYLabelWidthChanged(int w); + +private slots: + void signalUpdated(const cabana::Signal *sig); + void manageSeries(); + void handleMarkerClicked(); + void msgUpdated(MessageId id); + void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id == id; }); } + void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } + +private: + void createToolButtons(); + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *ev) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); } + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void leaveEvent(QEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + QSize sizeHint() const override; + void updateAxisY(); + void updateTitle(); + void resetChartCache(); + void setTheme(QChart::ChartTheme theme); + void paintEvent(QPaintEvent *event) override; + void drawForeground(QPainter *painter, const QRectF &rect) override; + void drawBackground(QPainter *painter, const QRectF &rect) override; + void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } + std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); + qreal niceNumber(qreal x, bool ceiling); + QXYSeries *createSeries(SeriesType type, QColor color); + void updateSeriesPoints(); + void removeIf(std::function predicate); + inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } + + int y_label_width = 0; + int align_to = 0; + QValueAxis *axis_x; + QValueAxis *axis_y; + QGraphicsPixmapItem *move_icon; + QGraphicsProxyWidget *close_btn_proxy; + QGraphicsProxyWidget *manage_btn_proxy; + TipLabel tip_label; + QList sigs; + double cur_sec = 0; + SeriesType series_type = SeriesType::Line; + bool is_scrubbing = false; + bool resume_after_scrub = false; + QPixmap chart_pixmap; + bool can_drop = false; + double tooltip_x = -1; + ChartsWidget *charts_widget; + friend class ChartsWidget; +}; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc new file mode 100644 index 0000000000..dab9687b5a --- /dev/null +++ b/tools/cabana/chart/chartswidget.cc @@ -0,0 +1,497 @@ +#include "tools/cabana/chart/chartswidget.h" + +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/chart/chart.h" + +const int MAX_COLUMN_COUNT = 4; +const int CHART_SPACING = 10; + +ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_timer(this), QFrame(parent) { + setFrameStyle(QFrame::StyledPanel | QFrame::Plain); + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->setContentsMargins(0, 0, 0, 0); + main_layout->setSpacing(0); + + // toolbar + QToolBar *toolbar = new QToolBar(tr("Charts"), this); + int icon_size = style()->pixelMetric(QStyle::PM_SmallIconSize); + toolbar->setIconSize({icon_size, icon_size}); + + auto new_plot_btn = new ToolButton("file-plus", tr("New Chart")); + auto new_tab_btn = new ToolButton("window-stack", tr("New Tab")); + toolbar->addWidget(new_plot_btn); + toolbar->addWidget(new_tab_btn); + toolbar->addWidget(title_label = new QLabel()); + title_label->setContentsMargins(0, 0, style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing), 0); + + QMenu *menu = new QMenu(this); + for (int i = 0; i < MAX_COLUMN_COUNT; ++i) { + menu->addAction(tr("%1").arg(i + 1), [=]() { setColumnCount(i + 1); }); + } + columns_action = toolbar->addAction(""); + columns_action->setMenu(menu); + qobject_cast(toolbar->widgetForAction(columns_action))->setPopupMode(QToolButton::InstantPopup); + + QLabel *stretch_label = new QLabel(this); + stretch_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + toolbar->addWidget(stretch_label); + + range_lb_action = toolbar->addWidget(range_lb = new QLabel(this)); + range_slider = new LogSlider(1000, Qt::Horizontal, this); + range_slider->setMaximumWidth(200); + range_slider->setToolTip(tr("Set the chart range")); + range_slider->setRange(1, settings.max_cached_minutes * 60); + range_slider->setSingleStep(1); + range_slider->setPageStep(60); // 1 min + range_slider_action = toolbar->addWidget(range_slider); + + // zoom controls + zoom_undo_stack = new QUndoStack(this); + toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this)); + undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); + toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this)); + redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); + reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + + toolbar->addWidget(remove_all_btn = new ToolButton("x", tr("Remove all charts"))); + toolbar->addWidget(dock_btn = new ToolButton("")); + main_layout->addWidget(toolbar); + + // tabbar + tabbar = new QTabBar(this); + tabbar->setAutoHide(true); + tabbar->setExpanding(false); + tabbar->setDrawBase(true); + tabbar->setAcceptDrops(true); + tabbar->setChangeCurrentOnDrag(true); + tabbar->setTabsClosable(true); + tabbar->setUsesScrollButtons(true); + main_layout->addWidget(tabbar); + + // charts + charts_container = new ChartsContainer(this); + + charts_scroll = new QScrollArea(this); + charts_scroll->setFrameStyle(QFrame::NoFrame); + charts_scroll->setWidgetResizable(true); + charts_scroll->setWidget(charts_container); + charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + main_layout->addWidget(charts_scroll); + + // init settings + current_theme = settings.theme; + column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT); + max_chart_range = std::clamp(settings.chart_range, 1, settings.max_cached_minutes * 60); + display_range = {0, max_chart_range}; + range_slider->setValue(max_chart_range); + updateToolBar(); + + align_timer.setSingleShot(true); + QObject::connect(&align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); + QObject::connect(&auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll); + QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); + QObject::connect(can, &AbstractStream::updated, this, &ChartsWidget::updateState); + QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); + QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); + QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); + QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); + QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); + QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); + QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); + QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { + if (index != -1) updateLayout(true); + }); + QObject::connect(dock_btn, &QToolButton::clicked, [this]() { + emit dock(!docking); + docking = !docking; + updateToolBar(); + }); + + newTab(); + setWhatsThis(tr(R"( + Chart view
+ + )")); +} + +void ChartsWidget::newTab() { + static int tab_unique_id = 0; + int idx = tabbar->addTab(""); + tabbar->setTabData(idx, tab_unique_id++); + for (int i = 0; i < tabbar->count(); ++i) { + tabbar->setTabText(i, QString("Tab %1").arg(i + 1)); + } + tabbar->setCurrentIndex(idx); +} + +void ChartsWidget::removeTab(int index) { + int id = tabbar->tabData(index).toInt(); + for (auto &c : tab_charts[id]) { + removeChart(c); + } + tab_charts.erase(id); + tabbar->removeTab(index); +} + +void ChartsWidget::eventsMerged() { + QFutureSynchronizer future_synchronizer; + for (auto c : charts) { + future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::updateSeries, nullptr)); + } +} + +void ChartsWidget::setZoom(double min, double max) { + zoomed_range = {min, max}; + is_zoomed = zoomed_range != display_range; + updateToolBar(); + updateState(); + emit rangeChanged(min, max, is_zoomed); +} + +void ChartsWidget::zoomReset() { + setZoom(display_range.first, display_range.second); + zoom_undo_stack->clear(); +} + +void ChartsWidget::showValueTip(double sec) { + const QRect visible_rect(-charts_container->pos(), charts_scroll->viewport()->size()); + for (auto c : currentCharts()) { + if (sec >= 0 && visible_rect.contains(QRect(c->mapTo(charts_container, QPoint(0, 0)), c->size()))) { + c->showTip(sec); + } else { + c->hideTip(); + } + } +} + +void ChartsWidget::updateState() { + if (charts.isEmpty()) return; + + const double cur_sec = can->currentSec(); + if (!is_zoomed) { + double pos = (cur_sec - display_range.first) / std::max(1.0, max_chart_range); + if (pos < 0 || pos > 0.8) { + display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); + } + double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->lastEventSecond()); + display_range.first = std::max(0.0, max_sec - max_chart_range); + display_range.second = display_range.first + max_chart_range; + } else if (cur_sec < zoomed_range.first || cur_sec >= zoomed_range.second) { + // loop in zoomed range + can->seekTo(zoomed_range.first); + } + + const auto &range = is_zoomed ? zoomed_range : display_range; + for (auto c : charts) { + c->updatePlot(cur_sec, range.first, range.second); + } +} + +void ChartsWidget::setMaxChartRange(int value) { + max_chart_range = settings.chart_range = range_slider->value(); + updateToolBar(); + updateState(); +} + +void ChartsWidget::updateToolBar() { + title_label->setText(tr("Charts: %1").arg(charts.size())); + columns_action->setText(tr("Column: %1").arg(column_count)); + range_lb->setText(utils::formatSeconds(max_chart_range)); + range_lb_action->setVisible(!is_zoomed); + range_slider_action->setVisible(!is_zoomed); + undo_zoom_action->setVisible(is_zoomed); + redo_zoom_action->setVisible(is_zoomed); + reset_zoom_action->setVisible(is_zoomed); + reset_zoom_btn->setText(is_zoomed ? tr("%1-%2").arg(zoomed_range.first, 0, 'f', 1).arg(zoomed_range.second, 0, 'f', 1) : ""); + remove_all_btn->setEnabled(!charts.isEmpty()); + dock_btn->setIcon(docking ? "arrow-up-right-square" : "arrow-down-left-square"); + dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts")); +} + +void ChartsWidget::settingChanged() { + if (std::exchange(current_theme, settings.theme) != current_theme) { + undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); + redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); + auto theme = settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; + for (auto c : charts) { + c->setTheme(theme); + } + } + range_slider->setRange(1, settings.max_cached_minutes * 60); + for (auto c : charts) { + c->setFixedHeight(settings.chart_height); + c->setSeriesType((SeriesType)settings.chart_series_type); + c->resetChartCache(); + } +} + +ChartView *ChartsWidget::findChart(const MessageId &id, const cabana::Signal *sig) { + for (auto c : charts) + if (c->hasSeries(id, sig)) return c; + return nullptr; +} + +ChartView *ChartsWidget::createChart() { + auto chart = new ChartView(is_zoomed ? zoomed_range : display_range, this); + chart->setFixedHeight(settings.chart_height); + chart->setMinimumWidth(CHART_MIN_WIDTH); + chart->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + QObject::connect(chart, &ChartView::axisYLabelWidthChanged, &align_timer, qOverload<>(&QTimer::start)); + charts.push_front(chart); + currentCharts().push_front(chart); + updateLayout(true); + updateToolBar(); + return chart; +} + +void ChartsWidget::showChart(const MessageId &id, const cabana::Signal *sig, bool show, bool merge) { + ChartView *chart = findChart(id, sig); + if (show && !chart) { + chart = merge && currentCharts().size() > 0 ? currentCharts().front() : createChart(); + chart->addSeries(id, sig); + } else if (!show && chart) { + chart->removeIf([&](auto &s) { return s.msg_id == id && s.sig == sig; }); + } +} + +void ChartsWidget::setColumnCount(int n) { + n = std::clamp(n, 1, MAX_COLUMN_COUNT); + if (column_count != n) { + column_count = settings.chart_column_count = n; + updateToolBar(); + updateLayout(); + } +} + +void ChartsWidget::updateLayout(bool force) { + auto charts_layout = charts_container->charts_layout; + int n = MAX_COLUMN_COUNT; + for (; n > 1; --n) { + if ((n * CHART_MIN_WIDTH + (n - 1) * charts_layout->spacing()) < charts_layout->geometry().width()) break; + } + + bool show_column_cb = n > 1; + columns_action->setVisible(show_column_cb); + + n = std::min(column_count, n); + auto ¤t_charts = currentCharts(); + if ((current_charts.size() != charts_layout->count() || n != current_column_count) || force) { + current_column_count = n; + charts_container->setUpdatesEnabled(false); + for (auto c : charts) { + c->setVisible(false); + } + for (int i = 0; i < current_charts.size(); ++i) { + charts_layout->addWidget(current_charts[i], i / n, i % n); + current_charts[i]->setVisible(true); + } + charts_container->setUpdatesEnabled(true); + } +} + +void ChartsWidget::startAutoScroll() { + auto_scroll_timer.start(50); +} + +void ChartsWidget::stopAutoScroll() { + auto_scroll_timer.stop(); + auto_scroll_count = 0; +} + +void ChartsWidget::doAutoScroll() { + QScrollBar *scroll = charts_scroll->verticalScrollBar(); + if (auto_scroll_count < scroll->pageStep()) { + ++auto_scroll_count; + } + + int value = scroll->value(); + QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos()); + QRect area = charts_scroll->viewport()->rect(); + + if (pos.y() - area.top() < settings.chart_height / 2) { + scroll->setValue(value - auto_scroll_count); + } else if (area.bottom() - pos.y() < settings.chart_height / 2) { + scroll->setValue(value + auto_scroll_count); + } + bool vertical_unchanged = value == scroll->value(); + if (vertical_unchanged) { + stopAutoScroll(); + } else { + // mouseMoveEvent to updates the drag-selection rectangle + const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos); + const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos); + QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos, + Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt); + QApplication::sendEvent(charts_scroll->viewport(), &mm); + } +} + +void ChartsWidget::resizeEvent(QResizeEvent *event) { + QWidget::resizeEvent(event); + updateLayout(); +} + +void ChartsWidget::newChart() { + SignalSelector dlg(tr("New Chart"), this); + if (dlg.exec() == QDialog::Accepted) { + auto items = dlg.seletedItems(); + if (!items.isEmpty()) { + auto c = createChart(); + for (auto it : items) { + c->addSeries(it->msg_id, it->sig); + } + } + } +} + +void ChartsWidget::removeChart(ChartView *chart) { + charts.removeOne(chart); + chart->deleteLater(); + for (auto &[_, list] : tab_charts) { + list.removeOne(chart); + } + updateToolBar(); + updateLayout(true); + alignCharts(); + emit seriesChanged(); +} + +void ChartsWidget::removeAll() { + if (!charts.isEmpty()) { + for (auto c : charts) { + c->deleteLater(); + } + charts.clear(); + tab_charts.clear(); + updateToolBar(); + emit seriesChanged(); + } + while (tabbar->count() > 1) { + tabbar->removeTab(1); + } +} + +void ChartsWidget::alignCharts() { + int plot_left = 0; + for (auto c : charts) { + plot_left = std::max(plot_left, c->y_label_width); + } + plot_left = std::max((plot_left / 10) * 10 + 10, 50); + for (auto c : charts) { + c->updatePlotArea(plot_left); + } +} + +bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) { + if (obj != this && event->type() == QEvent::Close) { + emit dock_btn->clicked(); + return true; + } + return false; +} + +bool ChartsWidget::event(QEvent *event) { + bool back_button = false; + switch (event->type()) { + case QEvent::MouseButtonPress: { + QMouseEvent *ev = static_cast(event); + back_button = ev->button() == Qt::BackButton; + break; + } + case QEvent::NativeGesture: { + QNativeGestureEvent *ev = static_cast(event); + back_button = (ev->value() == 180); + break; + } + case QEvent::WindowActivate: + case QEvent::WindowDeactivate: + case QEvent::FocusIn: + case QEvent::FocusOut: + case QEvent::Leave: + showValueTip(-1); + break; + default: + break; + } + + if (back_button) { + zoom_undo_stack->undo(); + return true; + } + return QFrame::event(event); +} + +// ChartsContainer + +ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) { + setAcceptDrops(true); + QVBoxLayout *charts_main_layout = new QVBoxLayout(this); + charts_main_layout->setContentsMargins(0, 10, 0, 0); + charts_layout = new QGridLayout(); + charts_layout->setSpacing(CHART_SPACING); + charts_main_layout->addLayout(charts_layout); + charts_main_layout->addStretch(0); +} + +void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + event->acceptProposedAction(); + drawDropIndicator(event->pos()); + } +} + +void ChartsContainer::dropEvent(QDropEvent *event) { + if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) { + auto w = getDropAfter(event->pos()); + auto chart = qobject_cast(event->source()); + if (w != chart) { + for (auto &[_, list] : charts_widget->tab_charts) { + list.removeOne(chart); + } + int to = w ? charts_widget->currentCharts().indexOf(w) + 1 : 0; + charts_widget->currentCharts().insert(to, chart); + charts_widget->updateLayout(true); + event->acceptProposedAction(); + } + drawDropIndicator({}); + } +} + +void ChartsContainer::paintEvent(QPaintEvent *ev) { + if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) { + QRect r; + if (auto insert_after = getDropAfter(drop_indictor_pos)) { + QRect area = insert_after->geometry(); + r = QRect(area.left(), area.bottom() + 1, area.width(), CHART_SPACING); + } else { + r = geometry(); + r.setHeight(CHART_SPACING); + } + + const int margin = (CHART_SPACING - 2) / 2; + QPainterPath path; + path.addPolygon(QPolygonF({r.topLeft(), QPointF(r.left() + CHART_SPACING, r.top() + r.height() / 2), r.bottomLeft()})); + path.addPolygon(QPolygonF({r.topRight(), QPointF(r.right() - CHART_SPACING, r.top() + r.height() / 2), r.bottomRight()})); + + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing); + p.fillPath(path, palette().highlight()); + p.fillRect(r.adjusted(2, margin, -2, -margin), palette().highlight()); + } +} + +ChartView *ChartsContainer::getDropAfter(const QPoint &pos) const { + auto it = std::find_if(charts_widget->currentCharts().crbegin(), charts_widget->currentCharts().crend(), [&pos](auto c) { + auto area = c->geometry(); + return pos.x() >= area.left() && pos.x() <= area.right() && pos.y() >= area.bottom(); + }); + return it == charts_widget->currentCharts().crend() ? nullptr : *it; +} diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h new file mode 100644 index 0000000000..8fd0e87e51 --- /dev/null +++ b/tools/cabana/chart/chartswidget.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/chart/signalselector.h" +#include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/streams/abstractstream.h" + +const int CHART_MIN_WIDTH = 300; +const QString CHART_MIME_TYPE = "application/x-cabanachartview"; + +class ChartView; +class ChartsWidget; + +class ChartsContainer : public QWidget { +public: + ChartsContainer(ChartsWidget *parent); + void dragEnterEvent(QDragEnterEvent *event) override; + void dropEvent(QDropEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); } + void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } + void paintEvent(QPaintEvent *ev) override; + ChartView *getDropAfter(const QPoint &pos) const; + + QGridLayout *charts_layout; + ChartsWidget *charts_widget; + QPoint drop_indictor_pos; +}; + +class ChartsWidget : public QFrame { + Q_OBJECT + +public: + ChartsWidget(QWidget *parent = nullptr); + 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; } + +public slots: + void setColumnCount(int n); + void removeAll(); + void setZoom(double min, double max); + +signals: + void dock(bool floating); + void rangeChanged(double min, double max, bool is_zommed); + void seriesChanged(); + +private: + void resizeEvent(QResizeEvent *event) override; + bool event(QEvent *event) override; + void alignCharts(); + void newChart(); + ChartView *createChart(); + void removeChart(ChartView *chart); + void eventsMerged(); + void updateState(); + void zoomReset(); + void startAutoScroll(); + void stopAutoScroll(); + void doAutoScroll(); + void updateToolBar(); + void setMaxChartRange(int value); + void updateLayout(bool force = false); + void settingChanged(); + void showValueTip(double sec); + bool eventFilter(QObject *obj, QEvent *event) override; + void newTab(); + void removeTab(int index); + inline QList ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; } + ChartView *findChart(const MessageId &id, const cabana::Signal *sig); + + QLabel *title_label; + QLabel *range_lb; + LogSlider *range_slider; + QAction *range_lb_action; + QAction *range_slider_action; + bool docking = true; + ToolButton *dock_btn; + + QAction *undo_zoom_action; + QAction *redo_zoom_action; + QAction *reset_zoom_action; + ToolButton *reset_zoom_btn; + QUndoStack *zoom_undo_stack; + + ToolButton *remove_all_btn; + QList charts; + std::unordered_map> tab_charts; + QTabBar *tabbar; + ChartsContainer *charts_container; + QScrollArea *charts_scroll; + uint32_t max_chart_range = 0; + bool is_zoomed = false; + std::pair display_range; + std::pair zoomed_range; + QAction *columns_action; + int column_count = 1; + int current_column_count = 0; + int auto_scroll_count = 0; + QTimer auto_scroll_timer; + QTimer align_timer; + int current_theme = 0; + friend class ZoomCommand; + friend class ChartView; + friend class ChartsContainer; +}; + +class ZoomCommand : public QUndoCommand { +public: + ZoomCommand(ChartsWidget *charts, std::pair range) : charts(charts), range(range), QUndoCommand() { + prev_range = charts->is_zoomed ? charts->zoomed_range : charts->display_range; + setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 1).arg(range.second, 0, 'f', 1)); + } + void undo() override { charts->setZoom(prev_range.first, prev_range.second); } + void redo() override { charts->setZoom(range.first, range.second); } + ChartsWidget *charts; + std::pair prev_range, range; +}; + diff --git a/tools/cabana/chart/signalselector.cc b/tools/cabana/chart/signalselector.cc new file mode 100644 index 0000000000..1aa8fc5016 --- /dev/null +++ b/tools/cabana/chart/signalselector.cc @@ -0,0 +1,108 @@ +#include "tools/cabana/chart/signalselector.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/streams/abstractstream.h" + +SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { + setWindowTitle(title); + QGridLayout *main_layout = new QGridLayout(this); + + // left column + main_layout->addWidget(new QLabel(tr("Available Signals")), 0, 0); + main_layout->addWidget(msgs_combo = new QComboBox(this), 1, 0); + msgs_combo->setEditable(true); + msgs_combo->lineEdit()->setPlaceholderText(tr("Select a msg...")); + msgs_combo->setInsertPolicy(QComboBox::NoInsert); + msgs_combo->completer()->setCompletionMode(QCompleter::PopupCompletion); + msgs_combo->completer()->setFilterMode(Qt::MatchContains); + + main_layout->addWidget(available_list = new QListWidget(this), 2, 0); + + // buttons + QVBoxLayout *btn_layout = new QVBoxLayout(); + QPushButton *add_btn = new QPushButton(utils::icon("chevron-right"), "", this); + add_btn->setEnabled(false); + QPushButton *remove_btn = new QPushButton(utils::icon("chevron-left"), "", this); + remove_btn->setEnabled(false); + btn_layout->addStretch(0); + btn_layout->addWidget(add_btn); + btn_layout->addWidget(remove_btn); + btn_layout->addStretch(0); + main_layout->addLayout(btn_layout, 0, 1, 3, 1); + + // right column + main_layout->addWidget(new QLabel(tr("Selected Signals")), 0, 2); + main_layout->addWidget(selected_list = new QListWidget(this), 1, 2, 2, 1); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox, 3, 2); + + for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { + if (auto m = dbc()->msg(it.key())) { + msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(it.key().toString()), QVariant::fromValue(it.key())); + } + } + msgs_combo->model()->sort(0); + msgs_combo->setCurrentIndex(-1); + + QObject::connect(msgs_combo, qOverload(&QComboBox::currentIndexChanged), this, &SignalSelector::updateAvailableList); + QObject::connect(available_list, &QListWidget::currentRowChanged, [=](int row) { add_btn->setEnabled(row != -1); }); + QObject::connect(selected_list, &QListWidget::currentRowChanged, [=](int row) { remove_btn->setEnabled(row != -1); }); + QObject::connect(available_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::add); + QObject::connect(selected_list, &QListWidget::itemDoubleClicked, this, &SignalSelector::remove); + QObject::connect(add_btn, &QPushButton::clicked, [this]() { if (auto item = available_list->currentItem()) add(item); }); + QObject::connect(remove_btn, &QPushButton::clicked, [this]() { if (auto item = selected_list->currentItem()) remove(item); }); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +void SignalSelector::add(QListWidgetItem *item) { + auto it = (ListItem *)item; + addItemToList(selected_list, it->msg_id, it->sig, true); + delete item; +} + +void SignalSelector::remove(QListWidgetItem *item) { + auto it = (ListItem *)item; + if (it->msg_id == msgs_combo->currentData().value()) { + addItemToList(available_list, it->msg_id, it->sig); + } + delete item; +} + +void SignalSelector::updateAvailableList(int index) { + if (index == -1) return; + available_list->clear(); + MessageId msg_id = msgs_combo->itemData(index).value(); + auto selected_items = seletedItems(); + for (auto s : dbc()->msg(msg_id)->getSignals()) { + bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), [=, sig = s](auto it) { return it->msg_id == msg_id && it->sig == sig; }); + if (!is_selected) { + addItemToList(available_list, msg_id, s); + } + } +} + +void SignalSelector::addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name) { + QString text = QString(" %1").arg(getColor(sig).name(), sig->name); + if (show_msg_name) text += QString(" %0 %1").arg(msgName(id), id.toString()); + + QLabel *label = new QLabel(text); + label->setContentsMargins(5, 0, 5, 0); + auto new_item = new ListItem(id, sig, parent); + new_item->setSizeHint(label->sizeHint()); + parent->setItemWidget(new_item, label); +} + +QList SignalSelector::seletedItems() { + QList ret; + for (int i = 0; i < selected_list->count(); ++i) ret.push_back((ListItem *)selected_list->item(i)); + return ret; +} diff --git a/tools/cabana/chart/signalselector.h b/tools/cabana/chart/signalselector.h new file mode 100644 index 0000000000..f46779f044 --- /dev/null +++ b/tools/cabana/chart/signalselector.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/dbc/dbcmanager.h" + +class SignalSelector : public QDialog { +public: + struct ListItem : public QListWidgetItem { + ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {} + MessageId msg_id; + const cabana::Signal *sig; + }; + + SignalSelector(QString title, QWidget *parent); + QList seletedItems(); + inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } + +private: + void updateAvailableList(int index); + void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false); + void add(QListWidgetItem *item); + void remove(QListWidgetItem *item); + + QComboBox *msgs_combo; + QListWidget *available_list; + QListWidget *selected_list; +}; diff --git a/tools/cabana/chart/tiplabel.cc b/tools/cabana/chart/tiplabel.cc new file mode 100644 index 0000000000..db686e44fa --- /dev/null +++ b/tools/cabana/chart/tiplabel.cc @@ -0,0 +1,46 @@ +#include "tools/cabana/chart/tiplabel.h" + +#include +#include +#include + +#include "tools/cabana/settings.h" + +TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { + setForegroundRole(QPalette::ToolTipText); + setBackgroundRole(QPalette::ToolTipBase); + auto palette = QToolTip::palette(); + if (settings.theme != DARK_THEME) { + palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); + palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush + } + setPalette(palette); + ensurePolished(); + setMargin(1 + style()->pixelMetric(QStyle::PM_ToolTipLabelFrameWidth, nullptr, this)); + setAttribute(Qt::WA_ShowWithoutActivating); + setTextFormat(Qt::RichText); + setVisible(false); +} + +void TipLabel::showText(const QPoint &pt, const QString &text, int right_edge) { + setText(text); + if (!text.isEmpty()) { + QSize extra(1, 1); + resize(sizeHint() + extra); + QPoint tip_pos(pt.x() + 12, pt.y()); + if (tip_pos.x() + size().width() >= right_edge) { + tip_pos.rx() = pt.x() - size().width() - 12; + } + move(tip_pos); + } + setVisible(!text.isEmpty()); +} + +void TipLabel::paintEvent(QPaintEvent *ev) { + QStylePainter p(this); + QStyleOptionFrame opt; + opt.init(this); + p.drawPrimitive(QStyle::PE_PanelTipLabel, opt); + p.end(); + QLabel::paintEvent(ev); +} diff --git a/tools/cabana/chart/tiplabel.h b/tools/cabana/chart/tiplabel.h new file mode 100644 index 0000000000..568aa3d726 --- /dev/null +++ b/tools/cabana/chart/tiplabel.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +class TipLabel : public QLabel { +public: + TipLabel(QWidget *parent = nullptr); + void showText(const QPoint &pt, const QString &sec, int right_edge); + void paintEvent(QPaintEvent *ev) override; +}; diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h deleted file mode 100644 index 8ad440b876..0000000000 --- a/tools/cabana/chartswidget.h +++ /dev/null @@ -1,249 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" -#include "tools/cabana/streams/abstractstream.h" -using namespace QtCharts; - -const int CHART_MIN_WIDTH = 300; - -enum class SeriesType { - Line = 0, - StepLine, - Scatter -}; - -class ValueTipLabel : public QLabel { -public: - ValueTipLabel(QWidget *parent = nullptr); - void showText(const QPoint &pt, const QString &sec, int right_edge); - void paintEvent(QPaintEvent *ev) override; -}; - -class ChartsWidget; -class ChartView : public QChartView { - Q_OBJECT - -public: - ChartView(const std::pair &x_range, ChartsWidget *parent = nullptr); - void addSeries(const MessageId &msg_id, const cabana::Signal *sig); - bool hasSeries(const MessageId &msg_id, const cabana::Signal *sig) const; - void updateSeries(const cabana::Signal *sig = nullptr); - void updatePlot(double cur, double min, double max); - void setSeriesType(SeriesType type); - void updatePlotArea(int left, bool force = false); - void showTip(double sec); - void hideTip(); - - struct SigItem { - MessageId msg_id; - const cabana::Signal *sig = nullptr; - QXYSeries *series = nullptr; - QVector vals; - QVector step_vals; - uint64_t last_value_mono_time = 0; - QPointF track_pt{}; - SegmentTree segment_tree; - double min = 0; - double max = 0; - }; - -signals: - void axisYLabelWidthChanged(int w); - -private slots: - void signalUpdated(const cabana::Signal *sig); - void manageSeries(); - void handleMarkerClicked(); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id == id; }); } - void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); } - -private: - void createToolButtons(); - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *ev) override; - void dragEnterEvent(QDragEnterEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); } - void dragMoveEvent(QDragMoveEvent *event) override; - void dropEvent(QDropEvent *event) override; - void leaveEvent(QEvent *event) override; - void resizeEvent(QResizeEvent *event) override; - QSize sizeHint() const override { return {CHART_MIN_WIDTH, settings.chart_height}; } - void updateAxisY(); - void updateTitle(); - void resetChartCache(); - void setTheme(QChart::ChartTheme theme); - void paintEvent(QPaintEvent *event) override; - void drawForeground(QPainter *painter, const QRectF &rect) override; - void drawBackground(QPainter *painter, const QRectF &rect) override; - void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); } - std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); - qreal niceNumber(qreal x, bool ceiling); - QXYSeries *createSeries(SeriesType type, QColor color); - void updateSeriesPoints(); - void removeIf(std::function predicate); - inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; } - - int y_label_width = 0; - int align_to = 0; - QValueAxis *axis_x; - QValueAxis *axis_y; - QGraphicsPixmapItem *move_icon; - QGraphicsProxyWidget *close_btn_proxy; - QGraphicsProxyWidget *manage_btn_proxy; - ValueTipLabel tip_label; - QList sigs; - double cur_sec = 0; - SeriesType series_type = SeriesType::Line; - bool is_scrubbing = false; - bool resume_after_scrub = false; - QPixmap chart_pixmap; - bool can_drop = false; - double tooltip_x = -1; - ChartsWidget *charts_widget; - friend class ChartsWidget; -}; - -class ChartsContainer : public QWidget { -public: - ChartsContainer(ChartsWidget *parent); - void dragEnterEvent(QDragEnterEvent *event) override; - void dropEvent(QDropEvent *event) override; - void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); } - void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); } - void paintEvent(QPaintEvent *ev) override; - ChartView *getDropAfter(const QPoint &pos) const; - - QGridLayout *charts_layout; - ChartsWidget *charts_widget; - QPoint drop_indictor_pos; -}; - -class ChartsWidget : public QFrame { - Q_OBJECT - -public: - ChartsWidget(QWidget *parent = nullptr); - 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; } - -public slots: - void setColumnCount(int n); - void removeAll(); - void setZoom(double min, double max); - -signals: - void dock(bool floating); - void rangeChanged(double min, double max, bool is_zommed); - void seriesChanged(); - -private: - void resizeEvent(QResizeEvent *event) override; - bool event(QEvent *event) override; - void alignCharts(); - void newChart(); - ChartView *createChart(); - void removeChart(ChartView *chart); - void eventsMerged(); - void updateState(); - void zoomReset(); - void startAutoScroll(); - void stopAutoScroll(); - void doAutoScroll(); - void updateToolBar(); - void setMaxChartRange(int value); - void updateLayout(bool force = false); - void settingChanged(); - void showValueTip(double sec); - bool eventFilter(QObject *obj, QEvent *event) override; - void newTab(); - void removeTab(int index); - inline QList ¤tCharts() { return tab_charts[tabbar->tabData(tabbar->currentIndex()).toInt()]; } - ChartView *findChart(const MessageId &id, const cabana::Signal *sig); - - QLabel *title_label; - QLabel *range_lb; - LogSlider *range_slider; - QAction *range_lb_action; - QAction *range_slider_action; - bool docking = true; - ToolButton *dock_btn; - - QAction *undo_zoom_action; - QAction *redo_zoom_action; - QAction *reset_zoom_action; - ToolButton *reset_zoom_btn; - QUndoStack *zoom_undo_stack; - - ToolButton *remove_all_btn; - QList charts; - std::unordered_map> tab_charts; - QTabBar *tabbar; - ChartsContainer *charts_container; - QScrollArea *charts_scroll; - uint32_t max_chart_range = 0; - bool is_zoomed = false; - std::pair display_range; - std::pair zoomed_range; - QAction *columns_action; - int column_count = 1; - int current_column_count = 0; - int auto_scroll_count = 0; - QTimer auto_scroll_timer; - QTimer align_timer; - int current_theme = 0; - friend class ZoomCommand; - friend class ChartView; - friend class ChartsContainer; -}; - -class ZoomCommand : public QUndoCommand { -public: - ZoomCommand(ChartsWidget *charts, std::pair range) : charts(charts), range(range), QUndoCommand() { - prev_range = charts->is_zoomed ? charts->zoomed_range : charts->display_range; - setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 1).arg(range.second, 0, 'f', 1)); - } - void undo() override { charts->setZoom(prev_range.first, prev_range.second); } - void redo() override { charts->setZoom(range.first, range.second); } - ChartsWidget *charts; - std::pair prev_range, range; -}; - -class SeriesSelector : public QDialog { -public: - struct ListItem : public QListWidgetItem { - ListItem(const MessageId &msg_id, const cabana::Signal *sig, QListWidget *parent) : msg_id(msg_id), sig(sig), QListWidgetItem(parent) {} - MessageId msg_id; - const cabana::Signal *sig; - }; - - SeriesSelector(QString title, QWidget *parent); - QList seletedItems(); - inline void addSelected(const MessageId &id, const cabana::Signal *sig) { addItemToList(selected_list, id, sig, true); } - -private: - void updateAvailableList(int index); - void addItemToList(QListWidget *parent, const MessageId id, const cabana::Signal *sig, bool show_msg_name = false); - void add(QListWidgetItem *item); - void remove(QListWidgetItem *item); - - QComboBox *msgs_combo; - QListWidget *available_list; - QListWidget *selected_list; -}; diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index b7cfed376c..074ce3b4d3 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -6,7 +6,7 @@ #include "selfdrive/ui/qt/widgets/controls.h" #include "tools/cabana/binaryview.h" -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 991a34931d..3445642d4d 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -7,7 +7,7 @@ #include #include -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/detailwidget.h" #include "tools/cabana/messageswidget.h" diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index c40a0f176c..f404dd3346 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/tools/cabana/signalview.h b/tools/cabana/signalview.h index 9711ae47bc..a2102ff9a7 100644 --- a/tools/cabana/signalview.h +++ b/tools/cabana/signalview.h @@ -8,7 +8,7 @@ #include #include -#include "tools/cabana/chartswidget.h" +#include "tools/cabana/chart/chartswidget.h" class SignalModel : public QAbstractItemModel { Q_OBJECT