openpilot is an open source driver assistance system. openpilot performs the functions of Automated Lane Centering and Adaptive Cruise Control for over 200 supported car makes and models.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

638 lines
23 KiB

#include "tools/cabana/chartswidget.h"
#include <QFutureSynchronizer>
#include <QGraphicsLayout>
#include <QRubberBand>
#include <QToolBar>
#include <QToolButton>
#include <QtConcurrent>
// ChartsWidget
ChartsWidget::ChartsWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
// toolbar
QToolBar *toolbar = new QToolBar(tr("Charts"), this);
title_label = new QLabel();
title_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
toolbar->addWidget(title_label);
show_all_values_btn = toolbar->addAction("");
toolbar->addWidget(range_label = new QLabel());
reset_zoom_btn = toolbar->addAction("");
reset_zoom_btn->setToolTip(tr("Reset zoom (drag on chart to zoom X-Axis)"));
remove_all_btn = toolbar->addAction("");
remove_all_btn->setToolTip(tr("Remove all charts"));
dock_btn = toolbar->addAction("");
main_layout->addWidget(toolbar);
// charts
QWidget *charts_container = new QWidget(this);
charts_layout = new QVBoxLayout(charts_container);
charts_layout->addStretch();
QScrollArea *charts_scroll = new QScrollArea(this);
charts_scroll->setWidgetResizable(true);
charts_scroll->setWidget(charts_container);
charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
main_layout->addWidget(charts_scroll);
max_chart_range = settings.max_chart_x_range;
use_dark_theme = palette().color(QPalette::WindowText).value() > palette().color(QPalette::Background).value();
updateToolBar();
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &ChartsWidget::removeAll);
QObject::connect(can, &CANMessages::eventsMerged, this, &ChartsWidget::eventsMerged);
QObject::connect(can, &CANMessages::updated, this, &ChartsWidget::updateState);
QObject::connect(show_all_values_btn, &QAction::triggered, this, &ChartsWidget::showAllData);
QObject::connect(remove_all_btn, &QAction::triggered, this, &ChartsWidget::removeAll);
QObject::connect(reset_zoom_btn, &QAction::triggered, this, &ChartsWidget::zoomReset);
QObject::connect(dock_btn, &QAction::triggered, [this]() {
emit dock(!docking);
docking = !docking;
updateToolBar();
});
}
void ChartsWidget::eventsMerged() {
if (auto events = can->events(); events && !events->empty()) {
event_range.first = (events->front()->mono_time / (double)1e9) - can->routeStartTime();
event_range.second = (events->back()->mono_time / (double)1e9) - can->routeStartTime();
updateState();
}
}
void ChartsWidget::updateDisplayRange() {
auto prev_range = display_range;
double current_sec = can->currentSec();
if (current_sec < display_range.first || current_sec >= (display_range.second - 5)) {
// reached the end, or seeked to a timestamp out of range.
display_range.first = current_sec - 5;
}
display_range.first = std::floor(std::max(display_range.first, event_range.first) * 10.0) / 10.0;
display_range.second = std::floor(std::min(display_range.first + max_chart_range, event_range.second) * 10.0) / 10.0;
if (prev_range != display_range) {
QFutureSynchronizer<void> future_synchronizer;
for (auto c : charts)
future_synchronizer.addFuture(QtConcurrent::run(c, &ChartView::setEventsRange, display_range));
}
}
void ChartsWidget::zoomIn(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() {
zoomIn(display_range.first, display_range.second);
}
void ChartsWidget::updateState() {
if (charts.isEmpty()) return;
if (!is_zoomed) {
updateDisplayRange();
} else if (can->currentSec() < zoomed_range.first || can->currentSec() >= zoomed_range.second) {
can->seekTo(zoomed_range.first);
}
const auto &range = is_zoomed ? zoomed_range : display_range;
setUpdatesEnabled(false);
for (auto c : charts) {
c->setDisplayRange(range.first, range.second);
c->scene()->invalidate({}, QGraphicsScene::ForegroundLayer);
}
setUpdatesEnabled(true);
}
void ChartsWidget::showAllData() {
bool switch_to_show_all = max_chart_range == settings.max_chart_x_range;
max_chart_range = switch_to_show_all ? settings.cached_segment_limit * 60
: settings.max_chart_x_range;
max_chart_range = std::min(max_chart_range, (uint32_t)can->totalSeconds());
updateToolBar();
updateState();
}
void ChartsWidget::updateToolBar() {
int min_range = std::min(settings.max_chart_x_range, (int)can->totalSeconds());
bool displaying_all = max_chart_range != min_range;
show_all_values_btn->setText(tr("%1 minutes").arg(max_chart_range / 60));
show_all_values_btn->setToolTip(tr("Click to display %1 data").arg(displaying_all ? tr("%1 minutes").arg(min_range / 60) : tr("ALL cached")));
show_all_values_btn->setVisible(!is_zoomed);
remove_all_btn->setEnabled(!charts.isEmpty());
reset_zoom_btn->setEnabled(is_zoomed);
range_label->setText(is_zoomed ? tr("%1 - %2").arg(zoomed_range.first, 0, 'f', 2).arg(zoomed_range.second, 0, 'f', 2) : "");
title_label->setText(charts.size() > 0 ? tr("Charts (%1)").arg(charts.size()) : tr("Charts"));
dock_btn->setText(docking ? "" : "");
dock_btn->setToolTip(docking ? tr("Undock charts") : tr("Dock charts"));
}
ChartView *ChartsWidget::findChart(const QString &id, const Signal *sig) {
for (auto c : charts)
if (c->hasSeries(id, sig)) return c;
return nullptr;
}
void ChartsWidget::showChart(const QString &id, const Signal *sig, bool show, bool merge) {
setUpdatesEnabled(false);
if (show) {
ChartView *chart = merge && charts.size() > 0 ? charts.back() : nullptr;
if (!chart) {
chart = new ChartView(this);
chart->chart()->setTheme(use_dark_theme ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight);
chart->setEventsRange(display_range);
auto range = is_zoomed ? zoomed_range : display_range;
chart->setDisplayRange(range.first, range.second);
QObject::connect(chart, &ChartView::remove, [=]() { removeChart(chart); });
QObject::connect(chart, &ChartView::zoomIn, this, &ChartsWidget::zoomIn);
QObject::connect(chart, &ChartView::zoomReset, this, &ChartsWidget::zoomReset);
QObject::connect(chart, &ChartView::seriesRemoved, this, &ChartsWidget::chartClosed);
charts_layout->insertWidget(0, chart);
charts.push_back(chart);
}
chart->addSeries(id, sig);
emit chartOpened(id, sig);
} else if (ChartView *chart = findChart(id, sig)) {
chart->removeSeries(id, sig);
}
updateToolBar();
setUpdatesEnabled(true);
}
void ChartsWidget::removeChart(ChartView *chart) {
charts.removeOne(chart);
chart->deleteLater();
updateToolBar();
}
void ChartsWidget::removeAll() {
for (auto c : charts.toVector())
removeChart(c);
}
bool ChartsWidget::eventFilter(QObject *obj, QEvent *event) {
if (obj != this && event->type() == QEvent::Close) {
emit dock_btn->triggered();
return true;
}
return false;
}
// ChartView
ChartView::ChartView(QWidget *parent) : QChartView(nullptr, parent) {
QChart *chart = new QChart();
chart->setBackgroundRoundness(0);
axis_x = new QValueAxis(this);
axis_y = new QValueAxis(this);
chart->addAxis(axis_x, Qt::AlignBottom);
chart->addAxis(axis_y, Qt::AlignLeft);
chart->legend()->setShowToolTips(true);
chart->layout()->setContentsMargins(0, 0, 0, 0);
chart->setMargins(QMargins(20, 11, 11, 11));
track_line = new QGraphicsLineItem(chart);
track_line->setPen(QPen(Qt::darkGray, 1, Qt::DashLine));
track_ellipse = new QGraphicsEllipseItem(chart);
track_ellipse->setBrush(Qt::darkGray);
value_text = new QGraphicsTextItem(chart);
item_group = scene()->createItemGroup({track_line, track_ellipse, value_text});
item_group->setZValue(chart->zValue() + 10);
// title
QToolButton *remove_btn = new QToolButton();
remove_btn->setText("X");
remove_btn->setAutoRaise(true);
remove_btn->setToolTip(tr("Remove Chart"));
close_btn_proxy = new QGraphicsProxyWidget(chart);
close_btn_proxy->setWidget(remove_btn);
close_btn_proxy->setZValue(chart->zValue() + 11);
QToolButton *manage_btn = new QToolButton();
manage_btn->setText("🔧");
manage_btn->setAutoRaise(true);
manage_btn->setToolTip(tr("Manage series"));
manage_btn_proxy = new QGraphicsProxyWidget(chart);
manage_btn_proxy->setWidget(manage_btn);
manage_btn_proxy->setZValue(chart->zValue() + 11);
setChart(chart);
setRenderHint(QPainter::Antialiasing);
setRubberBand(QChartView::HorizontalRubberBand);
updateFromSettings();
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &ChartView::signalRemoved);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &ChartView::signalUpdated);
QObject::connect(dbc(), &DBCManager::msgRemoved, this, &ChartView::msgRemoved);
QObject::connect(dbc(), &DBCManager::msgUpdated, this, &ChartView::msgUpdated);
QObject::connect(&settings, &Settings::changed, this, &ChartView::updateFromSettings);
QObject::connect(remove_btn, &QToolButton::clicked, this, &ChartView::remove);
QObject::connect(manage_btn, &QToolButton::clicked, this, &ChartView::manageSeries);
}
ChartView::~ChartView() {
for (auto &s : sigs)
emit seriesRemoved(s.msg_id, s.sig);
}
void ChartView::addSeries(const QString &msg_id, const Signal *sig) {
QLineSeries *series = new QLineSeries(this);
chart()->addSeries(series);
series->attachAxis(axis_x);
series->attachAxis(axis_y);
auto [source, address] = DBCManager::parseId(msg_id);
sigs.push_back({.msg_id = msg_id, .address = address, .source = source, .sig = sig, .series = series});
updateTitle();
updateSeries(sig);
updateAxisY();
}
void ChartView::removeSeries(const QString &msg_id, const Signal *sig) {
auto it = std::find_if(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
if (it != sigs.end()) {
it = removeSeries(it);
}
}
bool ChartView::hasSeries(const QString &msg_id, const Signal *sig) const {
return std::any_of(sigs.begin(), sigs.end(), [&](auto &s) { return s.msg_id == msg_id && s.sig == sig; });
}
QList<ChartView::SigItem>::iterator ChartView::removeSeries(const QList<ChartView::SigItem>::iterator &it) {
chart()->removeSeries(it->series);
it->series->deleteLater();
emit seriesRemoved(it->msg_id, it->sig);
auto ret = sigs.erase(it);
if (!sigs.isEmpty()) {
updateAxisY();
} else {
emit remove();
}
return ret;
}
void ChartView::signalUpdated(const Signal *sig) {
if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.sig == sig; })) {
updateTitle();
// TODO: don't update series if only name changed.
updateSeries(sig);
updateAxisY();
}
}
void ChartView::signalRemoved(const Signal *sig) {
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
it = (it->sig == sig) ? removeSeries(it) : ++it;
}
}
void ChartView::msgUpdated(uint32_t address) {
if (std::any_of(sigs.begin(), sigs.end(), [=](auto &s) { return s.address == address; }))
updateTitle();
}
void ChartView::msgRemoved(uint32_t address) {
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
it = (it->address == address) ? removeSeries(it) : ++it;
}
}
void ChartView::manageSeries() {
SeriesSelector dlg(this);
for (auto &s : sigs) {
dlg.addSeries(s.msg_id, msgName(s.msg_id), QString::fromStdString(s.sig->name));
}
int ret = dlg.exec();
if (ret == QDialog::Accepted) {
QList<QStringList> series_list = dlg.series();
if (series_list.isEmpty()) {
emit remove();
} else {
for (auto &s : series_list) {
if (auto m = dbc()->msg(s[0])) {
auto it = m->sigs.find(s[2]);
if (it != m->sigs.end() && !hasSeries(s[0], &(it->second))) {
addSeries(s[0], &(it->second));
}
}
}
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
bool exists = std::any_of(series_list.cbegin(), series_list.cend(), [&](auto &s) {
return s[0] == it->msg_id && s[2] == it->sig->name.c_str();
});
it = exists ? ++it : removeSeries(it);
}
}
}
}
void ChartView::resizeEvent(QResizeEvent *event) {
QChartView::resizeEvent(event);
int x = event->size().width() - close_btn_proxy->size().width() - 11;
close_btn_proxy->setPos(x, 8);
manage_btn_proxy->setPos(x - manage_btn_proxy->size().width() - 5, 8);
}
void ChartView::updateTitle() {
for (auto &s : sigs) {
s.series->setName(QString("<b>%1</b> <font color=\"gray\">%2 %3</font>").arg(s.sig->name.c_str()).arg(msgName(s.msg_id)).arg(s.msg_id));
}
}
void ChartView::updateFromSettings() {
setFixedHeight(settings.chart_height);
}
void ChartView::setEventsRange(const std::pair<double, double> &range) {
if (range != events_range) {
events_range = range;
updateSeries();
}
}
void ChartView::setDisplayRange(double min, double max) {
if (min != axis_x->min() || max != axis_x->max()) {
axis_x->setRange(min, max);
updateAxisY();
}
}
void ChartView::adjustChartMargins() {
// TODO: Remove hardcoded aligned_pos
const int aligned_pos = 60;
if ((int)chart()->plotArea().left() != aligned_pos) {
const float left_margin = chart()->margins().left() + aligned_pos - chart()->plotArea().left();
chart()->setMargins(QMargins(left_margin, 11, 11, 11));
scene()->invalidate({}, QGraphicsScene::ForegroundLayer);
}
}
void ChartView::updateSeries(const Signal *sig) {
auto events = can->events();
if (!events || sigs.isEmpty()) return;
for (auto &s : sigs) {
if (!sig || s.sig == sig) {
s.vals.clear();
s.vals.reserve((events_range.second - events_range.first) * 1000); // [n]seconds * 1000hz
s.min_y = std::numeric_limits<double>::max();
s.max_y = std::numeric_limits<double>::lowest();
double route_start_time = can->routeStartTime();
Event begin_event(cereal::Event::Which::INIT_DATA, (route_start_time + events_range.first) * 1e9);
auto begin = std::lower_bound(events->begin(), events->end(), &begin_event, Event::lessThan());
double end_ns = (route_start_time + events_range.second) * 1e9;
for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) {
if ((*it)->which == cereal::Event::Which::CAN) {
for (const auto &c : (*it)->event.getCan()) {
if (s.source == c.getSrc() && s.address == c.getAddress()) {
auto dat = c.getDat();
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *s.sig);
double ts = ((*it)->mono_time / (double)1e9) - route_start_time; // seconds
s.vals.push_back({ts, value});
if (value < s.min_y) s.min_y = value;
if (value > s.max_y) s.max_y = value;
}
}
}
}
s.series->replace(s.vals);
}
}
}
// auto zoom on yaxis
void ChartView::updateAxisY() {
if (sigs.isEmpty()) return;
double min_y = std::numeric_limits<double>::max();
double max_y = std::numeric_limits<double>::lowest();
if (events_range == std::pair{axis_x->min(), axis_x->max()}) {
for (auto &s : sigs) {
if (s.min_y < min_y) min_y = s.min_y;
if (s.max_y > max_y) max_y = s.max_y;
}
} else {
for (auto &s : sigs) {
auto begin = std::lower_bound(s.vals.begin(), s.vals.end(), axis_x->min(), [](auto &p, double x) { return p.x() < x; });
for (auto it = begin; it != s.vals.end() && it->x() <= axis_x->max(); ++it) {
if (it->y() < min_y) min_y = it->y();
if (it->y() > max_y) max_y = it->y();
}
}
}
if (min_y == std::numeric_limits<double>::max()) min_y = 0;
if (max_y == std::numeric_limits<double>::lowest()) max_y = 0;
if (max_y == min_y) {
axis_y->setRange(min_y - 1, max_y + 1);
} else {
double range = max_y - min_y;
applyNiceNumbers(min_y - range * 0.05, max_y + range * 0.05);
}
adjustChartMargins();
}
void ChartView::applyNiceNumbers(qreal min, qreal max) {
int tick_count = axis_y->tickCount();
qreal range = niceNumber((max - min), true); // range with ceiling
qreal step = niceNumber(range / (tick_count - 1), false);
min = qFloor(min / step);
max = qCeil(max / step);
tick_count = int(max - min) + 1;
axis_y->setRange(min * step, max * step);
axis_y->setTickCount(tick_count);
}
//nice numbers can be expressed as form of 1*10^n, 2* 10^n or 5*10^n
qreal ChartView::niceNumber(qreal x, bool ceiling) {
qreal z = qPow(10, qFloor(std::log10(x))); //find corresponding number of the form of 10^n than is smaller than x
qreal q = x / z; //q<10 && q>=1;
if (ceiling) {
if (q <= 1.0) q = 1;
else if (q <= 2.0) q = 2;
else if (q <= 5.0) q = 5;
else q = 10;
} else {
if (q < 1.5) q = 1;
else if (q < 3.0) q = 2;
else if (q < 7.0) q = 5;
else q = 10;
}
return q * z;
}
void ChartView::leaveEvent(QEvent *event) {
item_group->setVisible(false);
QChartView::leaveEvent(event);
}
void ChartView::mouseReleaseEvent(QMouseEvent *event) {
auto rubber = findChild<QRubberBand *>();
if (event->button() == Qt::LeftButton && rubber && rubber->isVisible()) {
rubber->hide();
QRectF rect = rubber->geometry().normalized();
double min = std::floor(chart()->mapToValue(rect.topLeft()).x() * 10.0) / 10.0;
double max = std::floor(chart()->mapToValue(rect.bottomRight()).x() * 10.0) / 10.0;
if (rubber->width() <= 0) {
// no rubber dragged, seek to mouse position
can->seekTo(min);
} else if ((max - min) >= 0.5) {
// zoom in if selected range is greater than 0.5s
emit zoomIn(min, max);
}
event->accept();
} else if (event->button() == Qt::RightButton) {
emit zoomReset();
event->accept();
} else {
QGraphicsView::mouseReleaseEvent(event);
}
}
void ChartView::mouseMoveEvent(QMouseEvent *ev) {
auto rubber = findChild<QRubberBand *>();
bool is_zooming = rubber && rubber->isVisible();
const auto plot_area = chart()->plotArea();
if (!is_zooming && plot_area.contains(ev->pos())) {
QStringList text_list;
QPointF pos = {};
const double sec = chart()->mapToValue(ev->pos()).x();
for (auto &s : sigs) {
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()) {
value = QString::number(it->y());
auto value_pos = chart()->mapToPosition(*it);
if (value_pos.x() > pos.x()) pos = value_pos;
}
text_list.push_back(QString("&nbsp;%1 : %2&nbsp;").arg(sigs.size() > 1 ? s.sig->name.c_str() : "Value").arg(value));
}
if (pos.x() == 0) pos = ev->pos();
QString time = QString::number(chart()->mapToValue(pos).x(), 'f', 3);
value_text->setHtml(QString("<div style=\"background-color: darkGray;color: white;\">&nbsp;Time: %1 &nbsp;<br />%2</div>")
.arg(time).arg(text_list.join("<br />")));
QRectF text_rect = value_text->boundingRect();
int text_x = pos.x() + 8;
if ((text_x + text_rect.width()) > plot_area.right()) {
text_x = pos.x() - text_rect.width() - 8;
}
value_text->setPos(text_x, pos.y() - text_rect.height() / 2);
track_line->setLine(pos.x(), plot_area.top(), pos.x(), plot_area.bottom());
track_ellipse->setRect(pos.x() - 5, pos.y() - 5, 10, 10);
item_group->setVisible(true);
} else {
item_group->setVisible(false);
}
QChartView::mouseMoveEvent(ev);
}
void ChartView::drawForeground(QPainter *painter, const QRectF &rect) {
qreal x = chart()->mapToPosition(QPointF{can->currentSec(), 0}).x();
painter->setPen(QPen(chart()->titleBrush().color(), 2));
painter->drawLine(QPointF{x, chart()->plotArea().top() - 2}, QPointF{x, chart()->plotArea().bottom() + 2});
}
// SeriesSelector
SeriesSelector::SeriesSelector(QWidget *parent) {
setWindowTitle(tr("Manage Chart Series"));
QHBoxLayout *contents_layout = new QHBoxLayout();
QVBoxLayout *left_layout = new QVBoxLayout();
left_layout->addWidget(new QLabel(tr("Select Signals:")));
msgs_combo = new QComboBox(this);
left_layout->addWidget(msgs_combo);
sig_list = new QListWidget(this);
sig_list->setSortingEnabled(true);
sig_list->setToolTip(tr("Double click on an item to add signal to chart"));
left_layout->addWidget(sig_list);
QVBoxLayout *right_layout = new QVBoxLayout();
right_layout->addWidget(new QLabel(tr("Chart Signals:")));
chart_series = new QListWidget(this);
chart_series->setSortingEnabled(true);
chart_series->setToolTip(tr("Double click on an item to remove signal from chart"));
right_layout->addWidget(chart_series);
contents_layout->addLayout(left_layout);
contents_layout->addLayout(right_layout);
auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->addLayout(contents_layout);
main_layout->addWidget(buttonBox);
for (auto it = can->can_msgs.cbegin(); it != can->can_msgs.cend(); ++it) {
if (auto m = dbc()->msg(it.key())) {
msgs_combo->addItem(QString("%1 (%2)").arg(m->name).arg(it.key()), it.key());
}
}
msgs_combo->model()->sort(0);
QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
QObject::connect(msgs_combo, SIGNAL(currentIndexChanged(int)), SLOT(msgSelected(int)));
QObject::connect(sig_list, &QListWidget::itemDoubleClicked, this, &SeriesSelector::addSignal);
QObject::connect(chart_series, &QListWidget::itemDoubleClicked, [](QListWidgetItem *item) { delete item; });
if (int index = msgs_combo->currentIndex(); index >= 0) {
msgSelected(index);
}
}
void SeriesSelector::msgSelected(int index) {
QString msg_id = msgs_combo->itemData(index).toString();
sig_list->clear();
if (auto m = dbc()->msg(msg_id)) {
for (auto &[name, s] : m->sigs) {
QStringList data({msg_id, m->name, name});
QListWidgetItem *item = new QListWidgetItem(name, sig_list);
item->setData(Qt::UserRole, data);
sig_list->addItem(item);
}
}
}
void SeriesSelector::addSignal(QListWidgetItem *item) {
QStringList data = item->data(Qt::UserRole).toStringList();
addSeries(data[0], data[1], data[2]);
}
void SeriesSelector::addSeries(const QString &id, const QString& msg_name, const QString &sig_name) {
QStringList data({id, msg_name, sig_name});
for (int i = 0; i < chart_series->count(); ++i) {
if (chart_series->item(i)->data(Qt::UserRole).toStringList() == data) {
return;
}
}
QListWidgetItem *new_item = new QListWidgetItem(chart_series);
new_item->setData(Qt::UserRole, data);
chart_series->addItem(new_item);
QLabel *label = new QLabel(QString("%0 <font color=\"gray\">%1 %2</font>").arg(data[2]).arg(data[1]).arg(data[0]), chart_series);
label->setContentsMargins(5, 0, 5, 0);
new_item->setSizeHint(label->sizeHint());
chart_series->setItemWidget(new_item, label);
}
QList<QStringList> SeriesSelector::series() {
QList<QStringList> ret;
for (int i = 0; i < chart_series->count(); ++i) {
ret.push_back(chart_series->item(i)->data(Qt::UserRole).toStringList());
}
return ret;
}