diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 8b47374fe3..23ea21f4fd 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +18,8 @@ #include "tools/cabana/chart/chartswidget.h" +// ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) +const int AXIS_X_TOP_MARGIN = 4; 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) { @@ -39,6 +42,7 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par setRubberBand(QChartView::HorizontalRubberBand); setMouseTracking(true); setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); + signal_value_font.setPointSize(9); QObject::connect(axis_y, &QValueAxis::rangeChanged, [this]() { resetChartCache(); }); QObject::connect(axis_y, &QAbstractAxis::titleTextChanged, [this]() { resetChartCache(); }); @@ -119,7 +123,7 @@ void ChartView::addSignal(const MessageId &msg_id, const cabana::Signal *sig) { } bool ChartView::hasSignal(const MessageId &msg_id, const cabana::Signal *sig) const { - return std::any_of(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); + return std::any_of(sigs.cbegin(), sigs.cend(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; }); } void ChartView::removeIf(std::function predicate) { @@ -143,14 +147,14 @@ void ChartView::removeIf(std::function predicate) { } void ChartView::signalUpdated(const cabana::Signal *sig) { - if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; })) { + if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.sig == sig; })) { updateTitle(); updateSeries(sig); } } void ChartView::msgUpdated(MessageId id) { - if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.msg_id == id; })) + if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.msg_id == id; })) updateTitle(); } @@ -189,10 +193,16 @@ void ChartView::updatePlotArea(int left_pos, bool force) { qreal left, top, right, bottom; chart()->layout()->getContentsMargins(&left, &top, &right, &bottom); - chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), manage_btn_proxy->sceneBoundingRect().bottomLeft()}); + QSizeF legend_size = chart()->legend()->layout()->minimumSize(); + legend_size.setWidth(manage_btn_proxy->sceneBoundingRect().left() - move_icon->sceneBoundingRect().right()); + chart()->legend()->setGeometry({move_icon->sceneBoundingRect().topRight(), legend_size}); + + // add top space for signal value + int adjust_top = chart()->legend()->geometry().height() + QFontMetrics(signal_value_font).height() + 3; + adjust_top = std::max(adjust_top, manage_btn_proxy->sceneBoundingRect().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin)); + // add right space for x-axis label QSizeF x_label_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, QString::number(axis_x->max(), 'f', 2)); x_label_size += QSizeF{5, 5}; - int adjust_top = chart()->legend()->geometry().height() + style()->pixelMetric(QStyle::PM_LayoutTopMargin); chart()->setPlotArea(rect().adjusted(align_to + left, adjust_top + top, -x_label_size.width() / 2 - right, -x_label_size.height() - bottom)); chart()->layout()->invalidate(); resetChartCache(); @@ -229,11 +239,11 @@ void ChartView::updatePlot(double cur, double min, double max) { void ChartView::updateSeriesPoints() { // Show points when zoomed in enough for (auto &s : sigs) { - auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto end = std::lower_bound(begin, s.vals.end(), axis_x->max(), xLessThan); + auto begin = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); + auto end = std::lower_bound(begin, s.vals.cend(), axis_x->max(), xLessThan); if (begin != end) { int num_points = std::max((end - begin), 1); - QPointF right_pt = end == s.vals.end() ? s.vals.back() : *end; + QPointF right_pt = end == s.vals.cend() ? s.vals.back() : *end; double pixels_per_point = (chart()->mapToPosition(right_pt).x() - chart()->mapToPosition(*begin).x()) / num_points; if (series_type == SeriesType::Scatter) { @@ -305,8 +315,8 @@ void ChartView::updateAxisY() { unit.clear(); } - auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); s.min = std::numeric_limits::max(); s.max = std::numeric_limits::lowest(); if (can->liveStreaming()) { @@ -315,7 +325,7 @@ void ChartView::updateAxisY() { if (it->y() > s.max) s.max = it->y(); } } else { - auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.begin(), first), std::distance(s.vals.begin(), last)); + auto [min_y, max_y] = s.segment_tree.minmax(std::distance(s.vals.cbegin(), first), std::distance(s.vals.cbegin(), last)); s.min = min_y; s.max = max_y; } @@ -530,8 +540,8 @@ void ChartView::showTip(double sec) { if (s.series->isVisible()) { QString value = "--"; // use reverse iterator to find last item <= sec. - auto it = std::lower_bound(s.vals.rbegin(), s.vals.rend(), sec, [](auto &p, double x) { return p.x() > x; }); - if (it != s.vals.rend() && it->x() >= axis_x->min()) { + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), sec, [](auto &p, double x) { return p.x() > x; }); + if (it != s.vals.crend() && it->x() >= axis_x->min()) { value = QString::number(it->y()); s.track_pt = *it; x = std::max(x, chart()->mapToPosition(*it).x()); @@ -640,14 +650,7 @@ void ChartView::drawBackground(QPainter *painter, const QRectF &rect) { } void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { - // draw time line - qreal x = chart()->mapToPosition(QPointF{cur_sec, 0}).x(); - x = std::clamp(x, chart()->plotArea().left(), chart()->plotArea().right()); - qreal y1 = chart()->plotArea().top() - 2; - qreal y2 = chart()->plotArea().bottom() + 2; - painter->setPen(QPen(chart()->titleBrush().color(), 2)); - painter->drawLine(QPointF{x, y1}, QPointF{x, y2}); - + drawTimeline(painter); // draw track points painter->setPen(Qt::NoPen); qreal track_line_x = -1; @@ -660,16 +663,17 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { } } if (track_line_x > 0) { + auto plot_area = chart()->plotArea(); painter->setPen(QPen(Qt::darkGray, 1, Qt::DashLine)); - painter->drawLine(QPointF{track_line_x, y1}, QPointF{track_line_x, y2}); + painter->drawLine(QPointF{track_line_x, plot_area.top()}, QPointF{track_line_x, plot_area.bottom()}); } // paint points. OpenGL mode lacks certain features (such as showing points) painter->setPen(Qt::NoPen); for (auto &s : sigs) { if (s.series->useOpenGL() && s.series->isVisible() && s.series->pointsVisible()) { - auto first = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), xLessThan); - auto last = std::lower_bound(first, s.vals.end(), axis_x->max(), xLessThan); + auto first = std::lower_bound(s.vals.cbegin(), s.vals.cend(), axis_x->min(), xLessThan); + auto last = std::lower_bound(first, s.vals.cend(), axis_x->max(), xLessThan); painter->setBrush(s.series->color()); for (auto it = first; it != last; ++it) { painter->drawEllipse(chart()->mapToPosition(*it), 4, 4); @@ -683,9 +687,8 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { painter->setPen(Qt::white); auto rubber_rect = rubber->geometry().normalized(); for (const auto &pt : {rubber_rect.bottomLeft(), rubber_rect.bottomRight()}) { - QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 1); - // ChartAxisElement's padding is 4 (https://codebrowser.dev/qt5/qtcharts/src/charts/axis/chartaxiselement_p.h.html) - auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -4, 6, 4); + QString sec = QString::number(chart()->mapToValue(pt).x(), 'f', 2); + auto r = painter->fontMetrics().boundingRect(sec).adjusted(-6, -AXIS_X_TOP_MARGIN, 6, AXIS_X_TOP_MARGIN); pt == rubber_rect.bottomLeft() ? r.moveTopRight(pt + QPoint{0, 2}) : r.moveTopLeft(pt + QPoint{0, 2}); painter->fillRect(r, Qt::gray); painter->drawText(r, Qt::AlignCenter, sec); @@ -693,6 +696,48 @@ void ChartView::drawForeground(QPainter *painter, const QRectF &rect) { } } +void ChartView::drawTimeline(QPainter *painter) { + const auto plot_area = chart()->plotArea(); + // draw line + qreal x = std::clamp(chart()->mapToPosition(QPointF{cur_sec, 0}).x(), plot_area.left(), plot_area.right()); + painter->setPen(QPen(chart()->titleBrush().color(), 2)); + painter->drawLine(QPointF{x, plot_area.top()}, QPointF{x, plot_area.bottom() + 1}); + + // draw current time + QString time_str = QString::number(cur_sec, 'f', 2); + QSize time_str_size = QFontMetrics(axis_x->labelsFont()).size(Qt::TextSingleLine, time_str) + QSize(8, 2); + QRect time_str_rect(QPoint(x - time_str_size.width() / 2, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); + QPainterPath path; + path.addRoundedRect(time_str_rect, 3, 3); + painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray); + painter->setPen(palette().color(QPalette::BrightText)); + painter->setFont(axis_x->labelsFont()); + painter->drawText(time_str_rect, Qt::AlignCenter, time_str); + + // draw signal value + auto item_group = qgraphicsitem_cast(chart()->legend()->childItems()[0]); + assert(item_group != nullptr); + auto legend_markers = item_group->childItems(); + assert(legend_markers.size() == sigs.size()); + + painter->setFont(signal_value_font); + painter->setPen(chart()->legend()->labelColor()); + int i = 0; + for (auto &s : sigs) { + QString value = "--"; + if (s.series->isVisible()) { + auto it = std::lower_bound(s.vals.crbegin(), s.vals.crend(), cur_sec, [](auto &p, double x) { return p.x() > x; }); + if (it != s.vals.crend() && it->x() >= axis_x->min()) { + value = s.sig->formatValue(it->y()); + } + } + QRectF marker_rect = legend_markers[i++]->sceneBoundingRect(); + QRectF value_rect(marker_rect.bottomLeft() - QPoint(0, 1), marker_rect.size()); + QString elided_val = painter->fontMetrics().elidedText(value, Qt::ElideRight, value_rect.width()); + painter->drawText(value_rect, Qt::AlignHCenter | Qt::AlignTop, elided_val); + } +} + QXYSeries *ChartView::createSeries(SeriesType type, QColor color) { QXYSeries *series = nullptr; if (type == SeriesType::Line) { diff --git a/tools/cabana/chart/chart.h b/tools/cabana/chart/chart.h index fda9271560..4170da4c95 100644 --- a/tools/cabana/chart/chart.h +++ b/tools/cabana/chart/chart.h @@ -80,6 +80,7 @@ private: 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(); } + void drawTimeline(QPainter *painter); std::tuple getNiceAxisNumbers(qreal min, qreal max, int tick_count); qreal niceNumber(qreal x, bool ceiling); QXYSeries *createSeries(SeriesType type, QColor color); @@ -104,6 +105,7 @@ private: QPixmap chart_pixmap; bool can_drop = false; double tooltip_x = -1; + QFont signal_value_font; ChartsWidget *charts_widget; friend class ChartsWidget; }; diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 0a87658c22..7221ebf5ce 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -60,7 +60,7 @@ ChartsWidget::ChartsWidget(QWidget *parent) : align_timer(this), auto_scroll_tim 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(remove_all_btn = new ToolButton("x-square", tr("Remove all charts"))); toolbar->addWidget(dock_btn = new ToolButton("")); main_layout->addWidget(toolbar); diff --git a/tools/cabana/chart/tiplabel.cc b/tools/cabana/chart/tiplabel.cc index 3ee5f00978..37c00834c3 100644 --- a/tools/cabana/chart/tiplabel.cc +++ b/tools/cabana/chart/tiplabel.cc @@ -9,6 +9,9 @@ TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setForegroundRole(QPalette::ToolTipText); setBackgroundRole(QPalette::ToolTipBase); + QFont font; + font.setPointSizeF(8.34563465); + setFont(font); auto palette = QToolTip::palette(); if (settings.theme != DARK_THEME) { palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index 46302ad789..909390fd27 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -17,6 +17,21 @@ void cabana::Signal::updatePrecision() { precision = std::max(num_decimals(factor), num_decimals(offset)); } +QString cabana::Signal::formatValue(double value) const { + // Show enum string + for (auto &[val, desc] : val_desc) { + if (std::abs(value - val.toInt()) < 1e-6) { + return desc; + } + } + + QString val_str = QString::number(value, 'f', precision); + if (!unit.isEmpty()) { + val_str += " " + unit; + } + return val_str; +} + // helper functions static QVector BIG_ENDIAN_START_BITS = []() { diff --git a/tools/cabana/dbc/dbc.h b/tools/cabana/dbc/dbc.h index 8b1e5a16e9..fdbbc9d169 100644 --- a/tools/cabana/dbc/dbc.h +++ b/tools/cabana/dbc/dbc.h @@ -57,6 +57,7 @@ namespace cabana { ValueDescription val_desc; int precision = 0; void updatePrecision(); + QString formatValue(double value) const; }; struct Msg { diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 32b0a39408..a35ec49432 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -595,17 +595,7 @@ void SignalView::updateState(const QHash *msgs) { const auto &last_msg = can->lastMessage(model->msg_id); for (auto item : model->root->children) { double value = get_raw_value((uint8_t *)last_msg.dat.constData(), last_msg.dat.size(), *item->sig); - item->sig_val = QString::number(value, 'f', item->sig->precision); - // Show unit - if (!item->sig->unit.isEmpty()) { - item->sig_val += " " + item->sig->unit; - } - // Show enum string - for (auto &[val, desc] : item->sig->val_desc) { - if (std::abs(value - val.toInt()) < 1e-6) { - item->sig_val = desc; - } - } + item->sig_val = item->sig->formatValue(value); max_value_width = std::max(max_value_width, fontMetrics().width(item->sig_val)); }