cabana: refactor and improve signal view (#26997)

* refactor and improve signal view

* cleanup undostack after save

* set clean

* fix logswidget::refresh

* fix bugs in historylog

* historylog:dont reset display_type on msg changed

* inline displaySignals

* historylog:add stretch to header

* remove sizePolicy

* always show toolbar

* add icon to tabwidget

* create new chart on top

* fix 'show plot' btn state sync issue

* rename removeSeries to removeItem

* historylog::fix displayRange

* MessageListModeL: improve setFilterString

* set as current index after expanded

* disable update during setmessage

* ChartWidget: fix issue in updateState

* skip align charts if there only one chart

* fix updateLayout

* cleanup historylog

* updateState before end reset model

* add validator

* improve sigmodel

* expand new signal

* add comment

* reserve vector

* cleanup condition

* single click on row to expand signal

* improve layout for canfd(64 bytes)

* cleanup

* return false on default

* show the latest signal value next to the signal name

* update sig value after setmessage.to make the value updated in pause mode.

* better size policy

* +1 for grid size

* better palette

* set autofillbg
pull/27131/head
Dean Lee 2 years ago committed by GitHub
parent fdc4a7f84c
commit 7a485d4308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      tools/cabana/binaryview.cc
  2. 6
      tools/cabana/binaryview.h
  3. 10
      tools/cabana/chartswidget.cc
  4. 2
      tools/cabana/chartswidget.h
  5. 11
      tools/cabana/commands.cc
  6. 6
      tools/cabana/commands.h
  7. 3
      tools/cabana/dbcmanager.cc
  8. 4
      tools/cabana/dbcmanager.h
  9. 172
      tools/cabana/detailwidget.cc
  10. 18
      tools/cabana/detailwidget.h
  11. 25
      tools/cabana/historylog.cc
  12. 5
      tools/cabana/historylog.h
  13. 22
      tools/cabana/mainwin.cc
  14. 1
      tools/cabana/mainwin.h
  15. 561
      tools/cabana/signaledit.cc
  16. 110
      tools/cabana/signaledit.h

@ -4,8 +4,10 @@
#include <QHeaderView>
#include <QMouseEvent>
#include <QPainter>
#include <QScrollBar>
#include <QToolTip>
#include "tools/cabana/commands.h"
#include "tools/cabana/streams/abstractstream.h"
// BinaryView
@ -30,12 +32,15 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
setFrameShape(QFrame::NoFrame);
setShowGrid(false);
setMouseTracking(true);
setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &BinaryView::refresh);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh);
}
QSize BinaryView::minimumSizeHint() const {
return {horizontalHeader()->minimumSectionSize() * 9 + VERTICAL_HEADER_WIDTH + 2, 0};
return {(horizontalHeader()->minimumSectionSize() + 1) * 9 + VERTICAL_HEADER_WIDTH,
CELL_HEIGHT * std::min(model->rowCount(), 10)};
}
void BinaryView::highlight(const Signal *sig) {
@ -124,13 +129,20 @@ void BinaryView::leaveEvent(QEvent *event) {
}
void BinaryView::setMessage(const QString &message_id) {
model->setMessage(message_id);
model->msg_id = message_id;
verticalScrollBar()->setValue(0);
refresh();
}
void BinaryView::refresh() {
if (model->msg_id.isEmpty()) return;
clearSelection();
anchor_index = QModelIndex();
resize_sig = nullptr;
hovered_sig = nullptr;
highlightPosition(QCursor::pos());
updateState();
model->refresh();
}
QSet<const Signal *> BinaryView::getOverlappingSignals() const {
@ -155,9 +167,8 @@ std::tuple<int, int, bool> BinaryView::getSelection(QModelIndex index) {
// BinaryViewModel
void BinaryViewModel::setMessage(const QString &message_id) {
void BinaryViewModel::refresh() {
beginResetModel();
msg_id = message_id;
items.clear();
if ((dbc_msg = dbc()->msg(msg_id))) {
row_count = dbc_msg->size;
@ -184,6 +195,7 @@ void BinaryViewModel::setMessage(const QString &message_id) {
items.resize(row_count * column_count);
}
endResetModel();
updateState();
}
void BinaryViewModel::updateState() {
@ -200,7 +212,7 @@ void BinaryViewModel::updateState() {
}
char hex[3] = {'\0'};
for (int i = 0; i < binary.size(); ++i) {
for (int j = 0; j < column_count - 1; ++j) {
for (int j = 0; j < 8; ++j) {
items[i * column_count + j].val = ((binary[i] >> (7 - j)) & 1) != 0 ? '1' : '0';
}
hex[0] = toHex(binary[i] >> 4);
@ -214,7 +226,7 @@ void BinaryViewModel::updateState() {
}
}
for (int i = 0; i < row_count * column_count; ++i) {
for (int i = 0; i < items.size(); ++i) {
if (i >= prev_items.size() || prev_items[i].val != items[i].val || prev_items[i].bg_color != items[i].bg_color) {
auto idx = index(i / column_count, i % column_count);
emit dataChanged(idx, idx);

@ -21,7 +21,7 @@ public:
class BinaryViewModel : public QAbstractTableModel {
public:
BinaryViewModel(QObject *parent) : QAbstractTableModel(parent) {}
void setMessage(const QString &message_id);
void refresh();
void updateState();
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const { return {}; }
@ -39,12 +39,11 @@ public:
QColor bg_color = QApplication::style()->standardPalette().color(QPalette::Base);
bool is_msb = false;
bool is_lsb = false;
QString val = "0";
QString val = "-";
QList<const Signal *> sigs;
};
std::vector<Item> items;
private:
QString msg_id;
const DBCMsg *dbc_msg = nullptr;
int row_count = 0;
@ -69,6 +68,7 @@ signals:
void resizeSignal(const Signal *sig, int from, int size);
private:
void refresh();
std::tuple<int, int, bool> getSelection(QModelIndex index);
void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) override;
void mousePressEvent(QMouseEvent *event) override;

@ -374,7 +374,7 @@ void ChartView::addSeries(const QString &msg_id, const Signal *sig) {
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);
it = removeItem(it);
}
}
@ -382,7 +382,7 @@ 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) {
QList<ChartView::SigItem>::iterator ChartView::removeItem(const QList<ChartView::SigItem>::iterator &it) {
chart()->removeSeries(it->series);
it->series->deleteLater();
QString msg_id = it->msg_id;
@ -407,7 +407,7 @@ void ChartView::signalUpdated(const Signal *sig) {
void ChartView::signalRemoved(const Signal *sig) {
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
it = (it->sig == sig) ? removeSeries(it) : ++it;
it = (it->sig == sig) ? removeItem(it) : ++it;
}
}
@ -418,7 +418,7 @@ void ChartView::msgUpdated(uint32_t address) {
void ChartView::msgRemoved(uint32_t address) {
for (auto it = sigs.begin(); it != sigs.end(); /**/) {
it = (it->address == address) ? removeSeries(it) : ++it;
it = (it->address == address) ? removeItem(it) : ++it;
}
}
@ -450,7 +450,7 @@ void ChartView::manageSeries() {
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);
it = exists ? ++it : removeItem(it);
}
}
}

@ -57,7 +57,7 @@ private slots:
void manageSeries();
private:
QList<ChartView::SigItem>::iterator removeSeries(const QList<ChartView::SigItem>::iterator &it);
QList<ChartView::SigItem>::iterator removeItem(const QList<ChartView::SigItem>::iterator &it);
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *ev) override;

@ -1,3 +1,5 @@
#include <QApplication>
#include "tools/cabana/commands.h"
// EditMsgCommand
@ -73,3 +75,12 @@ EditSignalCommand::EditSignalCommand(const QString &id, const Signal *sig, const
void EditSignalCommand::undo() { dbc()->updateSignal(id, new_signal.name.c_str(), old_signal); }
void EditSignalCommand::redo() { dbc()->updateSignal(id, old_signal.name.c_str(), new_signal); }
namespace UndoStack {
QUndoStack *instance() {
static QUndoStack *undo_stack = new QUndoStack(qApp);
return undo_stack;
}
} // namespace UndoStack

@ -1,6 +1,7 @@
#pragma once
#include <QUndoCommand>
#include <QUndoStack>
#include "tools/cabana/dbcmanager.h"
@ -60,3 +61,8 @@ private:
Signal old_signal = {};
Signal new_signal = {};
};
namespace UndoStack {
QUndoStack *instance();
inline void push(QUndoCommand *cmd) { instance()->push(cmd); }
};

@ -68,7 +68,7 @@ void DBCManager::addSignal(const QString &id, const Signal &sig) {
if (auto m = const_cast<DBCMsg *>(msg(id))) {
auto &s = m->sigs[sig.name.c_str()];
s = sig;
emit signalAdded(&s);
emit signalAdded(parseId(id).second, &s);
}
}
@ -110,6 +110,7 @@ DBCManager *dbc() {
std::vector<const Signal*> DBCMsg::getSignals() const {
std::vector<const Signal*> ret;
ret.reserve(sigs.size());
for (auto &[_, sig] : sigs) ret.push_back(&sig);
std::sort(ret.begin(), ret.end(), [](auto l, auto r) { return l->start_bit < r->start_bit; });
return ret;

@ -8,7 +8,9 @@
struct DBCMsg {
QString name;
uint32_t size;
// signal must be saved as value in map to make undo stack work properly.
std::map<QString, Signal> sigs;
// return vector<signals>, sort by start_bits
std::vector<const Signal*> getSignals() const;
};
@ -39,7 +41,7 @@ public:
}
signals:
void signalAdded(const Signal *sig);
void signalAdded(uint32_t address, const Signal *sig);
void signalRemoved(const Signal *sig);
void signalUpdated(const Signal *sig);
void msgUpdated(uint32_t address);

@ -4,8 +4,7 @@
#include <QFormLayout>
#include <QMenu>
#include <QMessageBox>
#include <QScrollBar>
#include <QTimer>
#include <QToolButton>
#include "selfdrive/ui/qt/util.h"
#include "tools/cabana/commands.h"
@ -15,7 +14,6 @@
// DetailWidget
DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) {
undo_stack = new QUndoStack(this);
QWidget *main_widget = new QWidget(this);
QVBoxLayout *main_layout = new QVBoxLayout(main_widget);
main_layout->setContentsMargins(0, 0, 0, 0);
@ -29,7 +27,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
main_layout->addWidget(tabbar);
// message title
toolbar = new QToolBar(this);
QToolBar *toolbar = new QToolBar(this);
toolbar->setIconSize({16, 16});
toolbar->addWidget(new QLabel("time:"));
time_label = new QLabel(this);
@ -54,41 +52,35 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
main_layout->addWidget(warning_widget);
// msg widget
QWidget *msg_widget = new QWidget(this);
QVBoxLayout *msg_layout = new QVBoxLayout(msg_widget);
msg_layout->setContentsMargins(0, 0, 0, 0);
// binary view
binary_view = new BinaryView(this);
msg_layout->addWidget(binary_view);
// signals
signals_layout = new QVBoxLayout();
signals_layout->setSpacing(0);
msg_layout->addLayout(signals_layout);
msg_layout->addStretch(0);
scroll = new QScrollArea(this);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setWidget(msg_widget);
scroll->setWidgetResizable(true);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
splitter = new QSplitter(Qt::Vertical, this);
splitter->setAutoFillBackground(true);
splitter->addWidget(binary_view = new BinaryView(this));
splitter->addWidget(signal_view = new SignalView(charts, this));
binary_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
signal_view->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
tab_widget = new QTabWidget(this);
tab_widget->setTabPosition(QTabWidget::South);
tab_widget->addTab(scroll, bootstrapPixmap("file-earmark-ruled"), "&Msg");
history_log = new LogsWidget(this);
tab_widget->addTab(history_log, bootstrapPixmap("stopwatch"), "&Logs");
tab_widget->addTab(splitter, bootstrapPixmap("file-earmark-ruled"), "&Msg");
tab_widget->addTab(history_log = new LogsWidget(this), bootstrapPixmap("stopwatch"), "&Logs");
main_layout->addWidget(tab_widget);
stacked_layout = new QStackedLayout(this);
stacked_layout->addWidget(new WelcomeWidget(this));
stacked_layout->addWidget(main_widget);
QObject::connect(binary_view, &BinaryView::signalClicked, this, &DetailWidget::showForm);
QObject::connect(binary_view, &BinaryView::resizeSignal, this, &DetailWidget::resizeSignal);
QObject::connect(binary_view, &BinaryView::addSignal, this, &DetailWidget::addSignal);
QObject::connect(binary_view, &BinaryView::resizeSignal, signal_view->model, &SignalModel::resizeSignal);
QObject::connect(binary_view, &BinaryView::addSignal, signal_view->model, &SignalModel::addSignal);
QObject::connect(binary_view, &BinaryView::signalHovered, signal_view, &SignalView::signalHovered);
QObject::connect(binary_view, &BinaryView::signalClicked, signal_view, &SignalView::expandSignal);
QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart);
QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight);
QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); });
QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { dbcMsgChanged(); });
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &DetailWidget::refresh);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh);
QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu);
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
if (index != -1 && tabbar->tabText(index) != msg_id) {
@ -96,14 +88,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
}
});
QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab);
QObject::connect(charts, &ChartsWidget::seriesChanged, this, &DetailWidget::updateChartState);
QObject::connect(history_log, &LogsWidget::openChart, [this](const QString &id, const Signal *sig) {
this->charts->showChart(id, sig, true, false);
});
QObject::connect(undo_stack, &QUndoStack::indexChanged, [this]() {
if (undo_stack->count() > 0)
dbcMsgChanged();
});
QObject::connect(charts, &ChartsWidget::seriesChanged, signal_view, &SignalView::updateChartState);
}
void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
@ -128,41 +113,27 @@ void DetailWidget::setMessage(const QString &message_id) {
index = tabbar->addTab(message_id);
tabbar->setTabToolTip(index, msgName(message_id));
}
tabbar->setCurrentIndex(index);
dbcMsgChanged();
scroll->verticalScrollBar()->setValue(0);
stacked_layout->setCurrentIndex(1);
}
void DetailWidget::dbcMsgChanged(int show_form_idx) {
if (msg_id.isEmpty()) return;
setUpdatesEnabled(false);
signal_view->setMessage(msg_id);
binary_view->setMessage(msg_id);
history_log->setMessage(msg_id);
int i = 0;
stacked_layout->setCurrentIndex(1);
tabbar->setCurrentIndex(index);
refresh();
splitter->setSizes({1, 2});
setUpdatesEnabled(true);
}
void DetailWidget::refresh() {
if (msg_id.isEmpty()) return;
QStringList warnings;
const DBCMsg *msg = dbc()->msg(msg_id);
if (msg) {
for (auto sig : msg->getSignals()) {
SignalEdit *form = i < signal_list.size() ? signal_list[i] : nullptr;
if (!form) {
form = new SignalEdit(i, this);
QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal);
QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal);
QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm);
QObject::connect(form, &SignalEdit::highlight, binary_view, &BinaryView::highlight);
QObject::connect(binary_view, &BinaryView::signalHovered, form, &SignalEdit::signalHovered);
QObject::connect(form, &SignalEdit::showChart, charts, &ChartsWidget::showChart);
signals_layout->addWidget(form);
signal_list.push_back(form);
}
form->setSignal(msg_id, sig);
form->setChartOpened(charts->hasSignal(msg_id, sig));
++i;
}
if (msg->size != can->lastMessage(msg_id).dat.size()) {
warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size));
}
@ -172,10 +143,6 @@ void DetailWidget::dbcMsgChanged(int show_form_idx) {
} else {
warnings.push_back(tr("Drag-Select in binary view to create new signal."));
}
for (/**/; i < signal_list.size(); ++i)
signal_list[i]->hide();
toolbar->setVisible(!msg_id.isEmpty());
remove_msg_act->setEnabled(msg != nullptr);
name_label->setText(msgName(msg_id));
@ -184,7 +151,6 @@ void DetailWidget::dbcMsgChanged(int show_form_idx) {
warning_icon->setPixmap(bootstrapPixmap(msg ? "exclamation-triangle" : "info-circle"));
}
warning_widget->setVisible(!warnings.isEmpty());
setUpdatesEnabled(true);
}
void DetailWidget::updateState(const QHash<QString, CanData> *msgs) {
@ -198,86 +164,18 @@ void DetailWidget::updateState(const QHash<QString, CanData> * msgs) {
history_log->updateState();
}
void DetailWidget::showForm(const Signal *sig) {
setUpdatesEnabled(false);
for (auto f : signal_list) {
f->updateForm(f->sig == sig && !f->form->isVisible());
if (f->sig == sig && f->form->isVisible()) {
QTimer::singleShot(0, [=]() { scroll->ensureWidgetVisible(f); });
}
}
setUpdatesEnabled(true);
}
void DetailWidget::updateChartState() {
for (auto f : signal_list)
f->setChartOpened(charts->hasSignal(f->msg_id, f->sig));
}
void DetailWidget::editMsg() {
QString id = msg_id;
auto msg = dbc()->msg(id);
int size = msg ? msg->size : can->lastMessage(id).dat.size();
EditMessageDialog dlg(id, msgName(id), size, this);
if (dlg.exec()) {
undo_stack->push(new EditMsgCommand(msg_id, dlg.name_edit->text(), dlg.size_spin->value()));
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text(), dlg.size_spin->value()));
}
}
void DetailWidget::removeMsg() {
undo_stack->push(new RemoveMsgCommand(msg_id));
}
void DetailWidget::addSignal(int start_bit, int size, bool little_endian) {
auto msg = dbc()->msg(msg_id);
if (!msg) {
for (int i = 1; /**/; ++i) {
QString name = QString("NEW_MSG_%1").arg(i);
auto it = std::find_if(dbc()->messages().begin(), dbc()->messages().end(), [&](auto &m) { return m.second.name == name; });
if (it == dbc()->messages().end()) {
undo_stack->push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size()));
msg = dbc()->msg(msg_id);
break;
}
}
}
Signal sig = {.is_little_endian = little_endian, .factor = 1};
for (int i = 1; /**/; ++i) {
sig.name = "NEW_SIGNAL_" + std::to_string(i);
if (msg->sigs.count(sig.name.c_str()) == 0) break;
}
updateSigSizeParamsFromRange(sig, start_bit, size);
undo_stack->push(new AddSigCommand(msg_id, sig));
}
void DetailWidget::resizeSignal(const Signal *sig, int start_bit, int size) {
Signal s = *sig;
updateSigSizeParamsFromRange(s, start_bit, size);
saveSignal(sig, s);
}
void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) {
auto msg = dbc()->msg(msg_id);
if (new_sig.name != sig->name) {
auto it = msg->sigs.find(new_sig.name.c_str());
if (it != msg->sigs.end()) {
QString warning_str = tr("There is already a signal with the same name '%1'").arg(new_sig.name.c_str());
QMessageBox::warning(this, tr("Failed to save signal"), warning_str);
return;
}
}
auto [start, end] = getSignalRange(&new_sig);
if (start < 0 || end >= msg->size * 8) {
QString warning_str = tr("Signal size [%1] exceed limit").arg(new_sig.size);
QMessageBox::warning(this, tr("Failed to save signal"), warning_str);
return;
}
undo_stack->push(new EditSignalCommand(msg_id, sig, new_sig));
}
void DetailWidget::removeSignal(const Signal *sig) {
undo_stack->push(new RemoveSigCommand(msg_id, sig));
UndoStack::push(new RemoveMsgCommand(msg_id));
}
// EditMessageDialog

@ -1,10 +1,9 @@
#pragma once
#include <QSplitter>
#include <QStackedLayout>
#include <QScrollArea>
#include <QTabWidget>
#include <QToolBar>
#include <QUndoStack>
#include "tools/cabana/binaryview.h"
#include "tools/cabana/chartswidget.h"
@ -30,18 +29,11 @@ class DetailWidget : public QWidget {
public:
DetailWidget(ChartsWidget *charts, QWidget *parent);
void setMessage(const QString &message_id);
void dbcMsgChanged(int show_form_idx = -1);
void refresh();
QSize minimumSizeHint() const override { return binary_view->minimumSizeHint(); }
QUndoStack *undo_stack = nullptr;
private:
void showForm(const Signal *sig);
void updateChartState();
void showTabBarContextMenu(const QPoint &pt);
void addSignal(int start_bit, int size, bool little_endian);
void resizeSignal(const Signal *sig, int from, int to);
void saveSignal(const Signal *sig, const Signal &new_sig);
void removeSignal(const Signal *sig);
void editMsg();
void removeMsg();
void updateState(const QHash<QString, CanData> * msgs = nullptr);
@ -49,15 +41,13 @@ private:
QString msg_id;
QLabel *name_label, *time_label, *warning_icon, *warning_label;
QWidget *warning_widget;
QVBoxLayout *signals_layout;
QTabBar *tabbar;
QTabWidget *tab_widget;
QToolBar *toolbar;
QAction *remove_msg_act;
LogsWidget *history_log;
BinaryView *binary_view;
QScrollArea *scroll;
SignalView *signal_view;
ChartsWidget *charts;
QSplitter *splitter;
QStackedLayout *stacked_layout;
QList<SignalEdit *> signal_list;
};

@ -4,6 +4,8 @@
#include <QPushButton>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
// HistoryLogModel
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
@ -22,17 +24,16 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
void HistoryLogModel::setMessage(const QString &message_id) {
msg_id = message_id;
sigs.clear();
if (auto dbc_msg = dbc()->msg(msg_id)) {
sigs = dbc_msg->getSignals();
}
filter_cmp = nullptr;
refresh();
}
void HistoryLogModel::refresh() {
beginResetModel();
sigs.clear();
if (auto dbc_msg = dbc()->msg(msg_id)) {
sigs = dbc_msg->getSignals();
}
last_fetch_time = 0;
has_more_data = true;
messages.clear();
hex_colors.clear();
updateState();
@ -74,7 +75,6 @@ void HistoryLogModel::setFilter(int sig_idx, const QString &value, std::function
filter_sig_idx = sig_idx;
filter_value = value.toDouble();
filter_cmp = value.isEmpty() ? nullptr : cmp;
refresh();
}
void HistoryLogModel::updateState() {
@ -232,11 +232,21 @@ LogsWidget::LogsWidget(QWidget *parent) : QWidget(parent) {
QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(setFilter()));
QObject::connect(value_edit, &QLineEdit::textChanged, this, &LogsWidget::setFilter);
QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::refresh);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &LogsWidget::refresh);
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &LogsWidget::refresh);
QObject::connect(can, &AbstractStream::eventsMerged, model, &HistoryLogModel::segmentsMerged);
}
void LogsWidget::setMessage(const QString &message_id) {
model->setMessage(message_id);
refresh();
}
void LogsWidget::refresh() {
if (model->msg_id.isEmpty()) return;
model->setFilter(0, "", nullptr);
model->refresh();
bool has_signal = model->sigs.size();
if (has_signal) {
signals_cb->clear();
@ -260,4 +270,5 @@ void LogsWidget::setFilter() {
case 3: cmp = std::less<double>{}; break;
}
model->setFilter(signals_cb->currentIndex(), value_edit->text(), cmp);
model->refresh();
}

@ -75,13 +75,12 @@ public:
void updateState() {if (dynamic_mode->isChecked()) model->updateState(); }
void showEvent(QShowEvent *event) override { if (dynamic_mode->isChecked()) model->refresh(); }
signals:
void openChart(const QString &msg_id, const Signal *sig);
private slots:
void setFilter();
private:
void refresh();
QTableView *logs;
HistoryLogModel *model;
QCheckBox *dynamic_mode;

@ -14,6 +14,8 @@
#include <QVBoxLayout>
#include <QWidgetAction>
#include "tools/cabana/commands.h"
static MainWindow *main_win = nullptr;
void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl;
@ -59,7 +61,7 @@ MainWindow::MainWindow() : QMainWindow() {
QObject::connect(charts_widget, &ChartsWidget::dock, this, &MainWindow::dockCharts);
QObject::connect(can, &AbstractStream::streamStarted, this, &MainWindow::loadDBCFromFingerprint);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged);
QObject::connect(detail_widget->undo_stack, &QUndoStack::cleanChanged, [this](bool clean) { setWindowModified(!clean); });
QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged);
}
void MainWindow::createActions() {
@ -98,16 +100,16 @@ void MainWindow::createActions() {
file_menu->addAction(tr("E&xit"), qApp, &QApplication::closeAllWindows)->setShortcuts(QKeySequence::Quit);
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
auto undo_act = detail_widget->undo_stack->createUndoAction(this, tr("&Undo"));
auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo"));
undo_act->setShortcuts(QKeySequence::Undo);
edit_menu->addAction(undo_act);
auto redo_act = detail_widget->undo_stack->createRedoAction(this, tr("&Rndo"));
auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Rndo"));
redo_act->setShortcuts(QKeySequence::Redo);
edit_menu->addAction(redo_act);
edit_menu->addSeparator();
QMenu *commands_menu = edit_menu->addMenu(tr("Command &List"));
auto undo_view = new QUndoView(detail_widget->undo_stack);
auto undo_view = new QUndoView(UndoStack::instance());
undo_view->setWindowTitle(tr("Command List"));
QWidgetAction *commands_act = new QWidgetAction(this);
commands_act->setDefaultWidget(undo_view);
@ -174,8 +176,12 @@ void MainWindow::createShortcuts() {
// TODO: add more shortcuts here.
}
void MainWindow::undoStackCleanChanged(bool clean) {
setWindowModified(!clean);
}
void MainWindow::DBCFileChanged() {
detail_widget->undo_stack->clear();
UndoStack::instance()->clear();
setWindowFilePath(QString("%1").arg(dbc()->name()));
}
@ -262,7 +268,7 @@ void MainWindow::saveFile(const QString &fn) {
QFile file(fn);
if (file.open(QIODevice::WriteOnly)) {
file.write(dbc()->generateDBC().toUtf8());
detail_widget->undo_stack->setClean();
UndoStack::instance()->setClean();
setCurrentFile(fn);
statusBar()->showMessage(tr("File saved"), 2000);
}
@ -309,7 +315,7 @@ void MainWindow::updateRecentFileActions() {
void MainWindow::remindSaveChanges() {
bool discard_changes = false;
while (!detail_widget->undo_stack->isClean() && !discard_changes) {
while (!UndoStack::instance()->isClean() && !discard_changes) {
int ret = (QMessageBox::question(this, tr("Unsaved Changes"),
tr("You have unsaved changes. Press ok to save them, cancel to discard."),
QMessageBox::Ok | QMessageBox::Cancel));
@ -319,7 +325,7 @@ void MainWindow::remindSaveChanges() {
discard_changes = true;
}
}
detail_widget->undo_stack->clear();
UndoStack::instance()->clear();
current_file = "";
}

@ -52,6 +52,7 @@ protected:
void updateDownloadProgress(uint64_t cur, uint64_t total, bool success);
void setOption();
void findSimilarBits();
void undoStackCleanChanged(bool clean);
VideoWidget *video_widget = nullptr;
QDockWidget *video_dock;

@ -1,174 +1,194 @@
#include "tools/cabana/signaledit.h"
#include <QDoubleValidator>
#include <QFormLayout>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QMessageBox>
#include <QPushButton>
#include <QToolButton>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
#include "selfdrive/ui/qt/util.h"
// SignalForm
// SignalModel
SignalForm::SignalForm(QWidget *parent) : QWidget(parent) {
auto double_validator = new QDoubleValidator(this);
double_validator->setLocale(QLocale::C); // Match locale of QString::toDouble() instead of system
SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) {
QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &SignalModel::refresh);
QObject::connect(dbc(), &DBCManager::msgUpdated, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::msgRemoved, this, &SignalModel::handleMsgChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, this, &SignalModel::handleSignalAdded);
QObject::connect(dbc(), &DBCManager::signalUpdated, this, &SignalModel::handleSignalUpdated);
QObject::connect(dbc(), &DBCManager::signalRemoved, this, &SignalModel::handleSignalRemoved);
QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalModel::updateState);
}
QVBoxLayout *main_layout = new QVBoxLayout(this);
QFormLayout *form_layout = new QFormLayout();
main_layout->addLayout(form_layout);
name = new QLineEdit();
name->setValidator(new NameValidator(name));
form_layout->addRow(tr("Name"), name);
QHBoxLayout *hl = new QHBoxLayout(this);
size = new QSpinBox();
size->setMinimum(1);
hl->addWidget(size);
endianness = new QComboBox();
endianness->addItems({"Little Endianness", "Big Endianness"});
hl->addWidget(endianness);
sign = new QComboBox();
sign->addItems({"Signed", "Unsigned"});
hl->addWidget(sign);
form_layout->addRow(tr("Size"), hl);
offset = new QLineEdit();
offset->setValidator(double_validator);
form_layout->addRow(tr("Offset"), offset);
factor = new QLineEdit();
factor->setValidator(double_validator);
form_layout->addRow(tr("Factor"), factor);
expand_btn = new QToolButton(this);
expand_btn->setText(tr("more..."));
main_layout->addWidget(expand_btn, 0, Qt::AlignRight);
// TODO: parse the following parameters in opendbc
QWidget *extra_container = new QWidget(this);
QFormLayout *extra_layout = new QFormLayout(extra_container);
unit = new QLineEdit();
extra_layout->addRow(tr("Unit"), unit);
comment = new QLineEdit();
extra_layout->addRow(tr("Comment"), comment);
min_val = new QLineEdit();
min_val->setValidator(double_validator);
extra_layout->addRow(tr("Minimum value"), min_val);
max_val = new QLineEdit();
max_val->setValidator(double_validator);
extra_layout->addRow(tr("Maximum value"), max_val);
val_desc = new QLineEdit();
extra_layout->addRow(tr("Value descriptions"), val_desc);
main_layout->addWidget(extra_container);
extra_container->setVisible(false);
QObject::connect(name, &QLineEdit::editingFinished, this, &SignalForm::textBoxEditingFinished);
QObject::connect(factor, &QLineEdit::editingFinished, this, &SignalForm::textBoxEditingFinished);
QObject::connect(offset, &QLineEdit::editingFinished, this, &SignalForm::textBoxEditingFinished);
QObject::connect(size, &QSpinBox::editingFinished, this, &SignalForm::changed);
QObject::connect(sign, SIGNAL(activated(int)), SIGNAL(changed()));
QObject::connect(endianness, SIGNAL(activated(int)), SIGNAL(changed()));
QObject::connect(expand_btn, &QToolButton::clicked, [=]() {
extra_container->setVisible(!extra_container->isVisible());
expand_btn->setText(extra_container->isVisible() ? tr("less...") : tr("more..."));
});
void SignalModel::insertItem(SignalModel::Item *parent_item, int pos, const Signal *sig) {
Item *item = new Item{.sig = sig, .parent = parent_item, .title = sig->name.c_str(), .type = Item::Sig};
parent_item->children.insert(pos, item);
QString titles[]{"Name", "Size", "Little Endian", "Signed", "Offset", "Factor", "Extra Info", "Unit", "Comment", "Minimum", "Maximum", "Description"};
for (int i = 0; i < std::size(titles); ++i) {
item->children.push_back(new Item{.sig = sig, .parent = item, .title = titles[i], .type = (Item::Type)(i + Item::Name)});
}
}
void SignalForm::textBoxEditingFinished() {
QLineEdit *edit = qobject_cast<QLineEdit *>(QObject::sender());
if (edit && edit->isModified()) {
edit->setModified(false);
emit changed();
void SignalModel::setMessage(const QString &id) {
msg_id = id;
filter_str = "";
refresh();
updateState(nullptr);
}
void SignalModel::setFilter(const QString &txt) {
filter_str = txt;
refresh();
}
// SignalEdit
void SignalModel::refresh() {
beginResetModel();
root.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id)) {
for (auto &s : msg->getSignals()) {
if (filter_str.isEmpty() || QString::fromStdString(s->name).contains(filter_str, Qt::CaseInsensitive)) {
insertItem(root.get(), root->children.size(), s);
}
}
}
endResetModel();
}
SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->setSpacing(0);
void SignalModel::updateState(const QHash<QString, CanData> *msgs) {
if (!msgs || (msgs->contains(msg_id))) {
auto &dat = can->lastMessage(msg_id).dat;
int row = 0;
for (auto item : root->children) {
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *item->sig);
item->sig_val = QString::number(value);
emit dataChanged(index(row, 1), index(row, 1), {Qt::DisplayRole});
++row;
}
}
}
bg_color = QColor(getColor(form_idx));
int SignalModel::rowCount(const QModelIndex &parent) const {
if (parent.column() > 0) return 0;
// title bar
auto title_bar = new QWidget(this);
title_bar->setFixedHeight(32);
QHBoxLayout *title_layout = new QHBoxLayout(title_bar);
title_layout->setContentsMargins(0, 0, 0, 0);
title_bar->setStyleSheet("QToolButton {width:15px;height:15px;font-size:15px}");
color_label = new QLabel(this);
color_label->setFixedWidth(25);
color_label->setContentsMargins(5, 0, 0, 0);
title_layout->addWidget(color_label);
icon = new QLabel(this);
title_layout->addWidget(icon);
title = new ElidedLabel(this);
title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
title_layout->addWidget(title);
plot_btn = new QToolButton(this);
plot_btn->setIcon(bootstrapPixmap("graph-up"));
plot_btn->setCheckable(true);
plot_btn->setAutoRaise(true);
title_layout->addWidget(plot_btn);
auto remove_btn = new QToolButton(this);
remove_btn->setAutoRaise(true);
remove_btn->setIcon(bootstrapPixmap("x"));
remove_btn->setToolTip(tr("Remove signal"));
title_layout->addWidget(remove_btn);
main_layout->addWidget(title_bar);
auto parent_item = getItem(parent);
int row_count = parent_item->children.size();
if (parent_item->type == Item::Sig && !parent_item->extra_expanded) {
row_count -= (Item::Desc - Item::ExtraInfo);
}
return row_count;
}
// signal form
form = new SignalForm(this);
form->setVisible(false);
main_layout->addWidget(form);
Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const {
if (!index.isValid()) return Qt::NoItemFlags;
// bottom line
QFrame *hline = new QFrame();
hline->setFrameShape(QFrame::HLine);
hline->setFrameShadow(QFrame::Sunken);
main_layout->addWidget(hline);
auto item = getItem(index);
Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
if (index.column() == 1 && item->type != Item::Sig && item->type != Item::ExtraInfo) {
flags |= (item->type == Item::Endian || item->type == Item::Signed) ? Qt::ItemIsUserCheckable : Qt::ItemIsEditable;
}
return flags;
}
QObject::connect(title, &ElidedLabel::clicked, [this]() { emit showFormClicked(sig); });
QObject::connect(plot_btn, &QToolButton::clicked, [this](bool checked) {
emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
});
QObject::connect(remove_btn, &QToolButton::clicked, [this]() { emit remove(sig); });
QObject::connect(form, &SignalForm::changed, this, &SignalEdit::saveSignal);
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
int SignalModel::signalRow(const Signal *sig) const {
auto &children = root->children;
for (int i = 0; i < children.size(); ++i) {
if (children[i]->sig == sig) return i;
}
return -1;
}
void SignalEdit::setSignal(const QString &message_id, const Signal *signal) {
sig = signal;
updateForm(msg_id == message_id && form->isVisible());
msg_id = message_id;
color_label->setText(QString::number(form_idx + 1));
color_label->setStyleSheet(QString("color:black; background-color:%2").arg(bg_color.name()));
title->setText(sig->name.c_str());
show();
QModelIndex SignalModel::index(int row, int column, const QModelIndex &parent) const {
if (!hasIndex(row, column, parent)) return {};
return createIndex(row, column, getItem(parent)->children[row]);
}
void SignalEdit::saveSignal() {
Signal s = *sig;
s.name = form->name->text().toStdString();
s.size = form->size->text().toInt();
s.offset = form->offset->text().toDouble();
s.factor = form->factor->text().toDouble();
s.is_signed = form->sign->currentIndex() == 0;
bool little_endian = form->endianness->currentIndex() == 0;
if (little_endian != s.is_little_endian) {
QModelIndex SignalModel::parent(const QModelIndex &index) const {
if (!index.isValid()) return {};
Item *parent_item = getItem(index)->parent;
return parent_item == root.get() ? QModelIndex() : createIndex(parent_item->row(), 0, parent_item);
}
QVariant SignalModel::data(const QModelIndex &index, int role) const {
if (index.isValid()) {
const Item *item = getItem(index);
if (role == Qt::DisplayRole || role == Qt::EditRole) {
if (index.column() == 0) {
return item->type == Item::Sig ? QString::fromStdString(item->sig->name) : item->title;
} else {
switch (item->type) {
case Item::Sig: return item->sig_val;
case Item::Name: return QString::fromStdString(item->sig->name);
case Item::Size: return item->sig->size;
case Item::Offset: return QString::number(item->sig->offset, 'f', 6);
case Item::Factor: return QString::number(item->sig->factor, 'f', 6);
default: break;
}
}
} else if (role == Qt::CheckStateRole && index.column() == 1) {
if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked;
if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked;
} else if (role == Qt::DecorationRole && index.column() == 0 && item->type == Item::ExtraInfo) {
return bootstrapPixmap(item->parent->extra_expanded ? "chevron-compact-down" : "chevron-compact-up");
}
}
return {};
}
bool SignalModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (role != Qt::EditRole && role != Qt::CheckStateRole) return false;
Item *item = getItem(index);
Signal s = *item->sig;
switch (item->type) {
case Item::Name: s.name = value.toString().toStdString(); break;
case Item::Size: s.size = value.toInt(); break;
case Item::Endian: s.is_little_endian = value.toBool(); break;
case Item::Signed: s.is_signed = value.toBool(); break;
case Item::Offset: s.offset = value.toDouble(); break;
case Item::Factor: s.factor = value.toDouble(); break;
default: return false;
}
bool ret = saveSignal(item->sig, s);
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
return ret;
}
void SignalModel::showExtraInfo(const QModelIndex &index) {
auto item = getItem(index);
if (item->type == Item::ExtraInfo) {
if (!item->parent->extra_expanded) {
item->parent->extra_expanded = true;
beginInsertRows(index.parent(), 7, 13);
endInsertRows();
} else {
item->parent->extra_expanded = false;
beginRemoveRows(index.parent(), 7, 13);
endRemoveRows();
}
}
}
bool SignalModel::saveSignal(const Signal *origin_s, Signal &s) {
auto msg = dbc()->msg(msg_id);
if (s.name != origin_s->name && msg->sigs.count(s.name.c_str()) != 0) {
QString text = tr("There is already a signal with the same name '%1'").arg(s.name.c_str());
QMessageBox::warning(nullptr, tr("Failed to save signal"), text);
return false;
}
if (s.is_little_endian != origin_s->is_little_endian) {
int start = std::floor(s.start_bit / 8);
if (little_endian) {
if (s.is_little_endian) {
int end = std::floor((s.start_bit - s.size + 1) / 8);
s.start_bit = start == end ? s.start_bit - s.size + 1 : bigEndianStartBitsIndex(s.start_bit);
} else {
int end = std::floor((s.start_bit + s.size - 1) / 8);
s.start_bit = start == end ? s.start_bit + s.size - 1 : bigEndianBitIndex(s.start_bit);
}
s.is_little_endian = little_endian;
}
if (s.is_little_endian) {
s.lsb = s.start_bit;
@ -177,43 +197,254 @@ void SignalEdit::saveSignal() {
s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1);
s.msb = s.start_bit;
}
if (s != *sig) {
emit save(this->sig, s);
UndoStack::push(new EditSignalCommand(msg_id, origin_s, s));
return true;
}
void SignalModel::addSignal(int start_bit, int size, bool little_endian) {
auto msg = dbc()->msg(msg_id);
for (int i = 1; !msg; ++i) {
QString name = QString("NEW_MSG_%1").arg(i);
if (std::none_of(dbc()->messages().begin(), dbc()->messages().end(), [&](auto &m) { return m.second.name == name; })) {
UndoStack::push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size()));
msg = dbc()->msg(msg_id);
}
}
Signal sig = {.is_little_endian = little_endian, .factor = 1};
for (int i = 1; /**/; ++i) {
sig.name = "NEW_SIGNAL_" + std::to_string(i);
if (msg->sigs.count(sig.name.c_str()) == 0) break;
}
updateSigSizeParamsFromRange(sig, start_bit, size);
UndoStack::push(new AddSigCommand(msg_id, sig));
}
void SignalModel::resizeSignal(const Signal *sig, int start_bit, int size) {
Signal s = *sig;
updateSigSizeParamsFromRange(s, start_bit, size);
saveSignal(sig, s);
}
void SignalModel::removeSignal(const Signal *sig) {
UndoStack::push(new RemoveSigCommand(msg_id, sig));
}
void SignalModel::handleMsgChanged(uint32_t address) {
if (address == DBCManager::parseId(msg_id).second) {
refresh();
}
}
void SignalModel::handleSignalAdded(uint32_t address, const Signal *sig) {
if (address == DBCManager::parseId(msg_id).second) {
int i = 0;
for (; i < root->children.size(); ++i) {
if (sig->start_bit < root->children[i]->sig->start_bit) break;
}
beginInsertRows({}, i, i);
insertItem(root.get(), i, sig);
endInsertRows();
}
}
void SignalModel::handleSignalUpdated(const Signal *sig) {
if (int row = signalRow(sig); row != -1) {
emit dataChanged(index(row, 0), index(row, 1), {Qt::DisplayRole, Qt::EditRole, Qt::CheckStateRole});
}
}
void SignalModel::handleSignalRemoved(const Signal *sig) {
if (int row = signalRow(sig); row != -1) {
beginRemoveRows({}, row, row);
delete root->children.takeAt(row);
endRemoveRows();
}
}
// SignalItemDelegate
SignalItemDelegate::SignalItemDelegate(QObject *parent) {
name_validator = new NameValidator(this);
double_validator = new QDoubleValidator(this);
double_validator->setLocale(QLocale::C); // Match locale of QString::toDouble() instead of system
}
void SignalItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto item = (SignalModel::Item *)index.internalPointer();
if (item && !index.parent().isValid() && index.column() == 0) {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
if (option.state & QStyle::State_Selected) {
painter->fillRect(option.rect, option.palette.highlight());
}
// color label
auto bg_color = QColor(getColor(item->row()));
QRect rc{option.rect.left() + 3, option.rect.top(), 22, option.rect.height()};
painter->setPen(Qt::NoPen);
painter->setBrush(item->highlight ? bg_color.darker(125) : bg_color);
painter->drawRoundedRect(rc.adjusted(0, 2, 0, -2), 5, 5);
painter->setPen(item->highlight ? Qt::white : Qt::black);
painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1));
// signal name
QFont font;
font.setBold(true);
painter->setFont(font);
painter->setPen((option.state & QStyle::State_Selected ? option.palette.highlightedText() : option.palette.text()).color());
painter->drawText(option.rect.adjusted(rc.width() + 9, 0, 0, 0), option.displayAlignment, index.data(Qt::DisplayRole).toString());
painter->restore();
} else {
QStyledItemDelegate::paint(painter, option, index);
}
}
void SignalEdit::setChartOpened(bool opened) {
plot_btn->setToolTip(opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened chart"));
plot_btn->setChecked(opened);
QWidget *SignalItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
auto item = (SignalModel::Item *)index.internalPointer();
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor) {
QLineEdit *e = new QLineEdit(parent);
e->setFrame(false);
e->setValidator(index.row() == 0 ? name_validator : double_validator);
return e;
} else if (item->type == SignalModel::Item::Size) {
QSpinBox *spin = new QSpinBox(parent);
spin->setFrame(false);
spin->setRange(1, 64);
return spin;
}
return QStyledItemDelegate::createEditor(parent, option, index);
}
// SignalView
SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) {
// title bar
QWidget *title_bar = new QWidget(this);
title_bar->setAutoFillBackground(true);
QHBoxLayout *hl = new QHBoxLayout(title_bar);
hl->addWidget(signal_count_lb = new QLabel());
filter_edit = new QLineEdit(this);
filter_edit->setClearButtonEnabled(true);
filter_edit->setPlaceholderText(tr("filter signals by name"));
hl->addWidget(filter_edit);
hl->addStretch(1);
auto collapse_btn = new QToolButton();
collapse_btn->setIcon(bootstrapPixmap("dash-square"));
collapse_btn->setIconSize({12, 12});
collapse_btn->setAutoRaise(true);
collapse_btn->setToolTip(tr("Collapse All"));
hl->addWidget(collapse_btn);
// tree view
tree = new QTreeView(this);
tree->setModel(model = new SignalModel(this));
tree->setItemDelegate(new SignalItemDelegate(this));
tree->setFrameShape(QFrame::NoFrame);
tree->setHeaderHidden(true);
tree->setMouseTracking(true);
tree->setExpandsOnDoubleClick(false);
tree->header()->setSectionResizeMode(QHeaderView::Stretch);
tree->setMinimumHeight(300);
tree->setStyleSheet("QSpinBox{background-color:white;border:none;} QLineEdit{background-color:white;}");
QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->setSpacing(0);
main_layout->addWidget(title_bar);
main_layout->addWidget(tree);
QObject::connect(filter_edit, &QLineEdit::textEdited, model, &SignalModel::setFilter);
QObject::connect(collapse_btn, &QPushButton::clicked, tree, &QTreeView::collapseAll);
QObject::connect(tree, &QAbstractItemView::clicked, this, &SignalView::rowClicked);
QObject::connect(tree, &QTreeView::viewportEntered, [this]() { emit highlight(nullptr); });
QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); });
QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged);
QObject::connect(model, &QAbstractItemModel::rowsInserted, this, &SignalView::rowsChanged);
QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged);
QObject::connect(dbc(), &DBCManager::signalAdded, [this](uint32_t address, const Signal *sig) { expandSignal(sig); });
}
void SignalView::setMessage(const QString &id) {
msg_id = id;
filter_edit->clear();
model->setMessage(id);
}
void SignalEdit::updateForm(bool visible) {
if (visible && sig) {
if (form->name->text() != sig->name.c_str()) {
form->name->setText(sig->name.c_str());
void SignalView::rowsChanged() {
auto create_btn = [](const QString &id, const QString &tooltip) {
auto btn = new QToolButton();
btn->setIcon(bootstrapPixmap(id));
btn->setToolTip(tooltip);
btn->setAutoRaise(true);
return btn;
};
signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount()));
for (int i = 0; i < model->rowCount(); ++i) {
auto index = model->index(i, 1);
if (!tree->indexWidget(index)) {
QWidget *w = new QWidget(this);
QHBoxLayout *h = new QHBoxLayout(w);
h->setContentsMargins(0, 2, 0, 2);
h->addStretch(1);
auto remove_btn = create_btn("x", tr("Remove signal"));
auto plot_btn = create_btn("graph-up", "");
plot_btn->setCheckable(true);
h->addWidget(plot_btn);
h->addWidget(remove_btn);
tree->setIndexWidget(index, w);
auto sig = model->getItem(index)->sig;
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { model->removeSignal(sig); });
QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) {
emit showChart(msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
});
}
form->endianness->setCurrentIndex(sig->is_little_endian ? 0 : 1);
form->sign->setCurrentIndex(sig->is_signed ? 0 : 1);
form->factor->setText(QString::number(sig->factor));
form->offset->setText(QString::number(sig->offset));
form->size->setValue(sig->size);
}
form->setVisible(visible);
icon->setText(visible ? "" : "> ");
updateChartState();
}
void SignalEdit::signalHovered(const Signal *s) {
auto text_color = sig == s ? "white" : "black";
auto _bg_color = sig == s ? bg_color.darker(125) : bg_color; // 4/5x brightness
color_label->setStyleSheet(QString("color:%1; background-color:%2").arg(text_color).arg(_bg_color.name()));
void SignalView::rowClicked(const QModelIndex &index) {
auto item = model->getItem(index);
if (item->type == SignalModel::Item::Sig) {
auto sig_index = model->index(index.row(), 0, index.parent());
tree->setExpanded(sig_index, !tree->isExpanded(sig_index));
} else if (item->type == SignalModel::Item::ExtraInfo) {
model->showExtraInfo(index);
}
}
void SignalEdit::enterEvent(QEvent *event) {
emit highlight(sig);
QWidget::enterEvent(event);
void SignalView::expandSignal(const Signal *sig) {
if (int row = model->signalRow(sig); row != -1) {
auto idx = model->index(row, 0);
bool expand = !tree->isExpanded(idx);
tree->setExpanded(idx, expand);
tree->scrollTo(idx, QAbstractItemView::PositionAtTop);
if (expand) tree->setCurrentIndex(idx);
}
}
void SignalEdit::leaveEvent(QEvent *event) {
emit highlight(nullptr);
QWidget::leaveEvent(event);
void SignalView::updateChartState() {
int i = 0;
for (auto item : model->root->children) {
auto plot_btn = tree->indexWidget(model->index(i, 1))->findChildren<QToolButton*>()[0];
bool chart_opened = charts->hasSignal(msg_id, item->sig);
plot_btn->setChecked(chart_opened);
plot_btn->setToolTip(chart_opened ? tr("Close Plot") : tr("Show Plot\nSHIFT click to add to previous opened plot"));
++i;
}
}
void SignalView::signalHovered(const Signal *sig) {
auto &children = model->root->children;
for (int i = 0; i < children.size(); ++i) {
bool highlight = children[i]->sig == sig;
if (std::exchange(children[i]->highlight, highlight) != highlight) {
emit model->dataChanged(model->index(i, 0), model->index(i, 0));
}
}
}

@ -1,59 +1,97 @@
#pragma once
#include <QComboBox>
#include <QAbstractItemModel>
#include <QLabel>
#include <QLineEdit>
#include <QSpinBox>
#include <QToolButton>
#include <QStyledItemDelegate>
#include <QTreeView>
#include "selfdrive/ui/qt/widgets/controls.h"
#include "tools/cabana/chartswidget.h"
#include "tools/cabana/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
class SignalForm : public QWidget {
class SignalModel : public QAbstractItemModel {
Q_OBJECT
public:
SignalForm(QWidget *parent);
void textBoxEditingFinished();
struct Item {
enum Type {Root, Sig, Name, Size, Endian, Signed, Offset, Factor, ExtraInfo, Unit, Comment, Min, Max, Desc };
~Item() { qDeleteAll(children); }
inline int row() { return parent->children.indexOf(this); }
QLineEdit *name, *unit, *comment, *val_desc, *offset, *factor, *min_val, *max_val;
QSpinBox *size;
QComboBox *sign, *endianness;
QToolButton *expand_btn;
Type type = Type::Root;
Item *parent = nullptr;
QList<Item *> children;
signals:
void changed();
const Signal *sig = nullptr;
QString title;
bool highlight = false;
bool extra_expanded = false;
QString sig_val = "-";
};
SignalModel(QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 2; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
void setMessage(const QString &id);
void setFilter(const QString &txt);
void addSignal(int start_bit, int size, bool little_endian);
bool saveSignal(const Signal *origin_s, Signal &s);
void resizeSignal(const Signal *sig, int start_bit, int size);
void removeSignal(const Signal *sig);
inline Item *getItem(const QModelIndex &index) const { return index.isValid() ? (Item *)index.internalPointer() : root.get(); }
int signalRow(const Signal *sig) const;
void showExtraInfo(const QModelIndex &index);
private:
void insertItem(SignalModel::Item *parent_item, int pos, const Signal *sig);
void handleSignalAdded(uint32_t address, const Signal *sig);
void handleSignalUpdated(const Signal *sig);
void handleSignalRemoved(const Signal *sig);
void handleMsgChanged(uint32_t address);
void refresh();
void updateState(const QHash<QString, CanData> *msgs);
QString msg_id;
QString filter_str;
std::unique_ptr<Item> root;
friend class SignalView;
};
class SignalEdit : public QWidget {
class SignalItemDelegate : public QStyledItemDelegate {
public:
SignalItemDelegate(QObject *parent);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
QValidator *name_validator, *double_validator;
};
class SignalView : public QWidget {
Q_OBJECT
public:
SignalEdit(int index, QWidget *parent = nullptr);
void setSignal(const QString &msg_id, const Signal *sig);
void setChartOpened(bool opened);
SignalView(ChartsWidget *charts, QWidget *parent);
void setMessage(const QString &id);
void signalHovered(const Signal *sig);
void updateForm(bool show);
const Signal *sig = nullptr;
SignalForm *form = nullptr;
QString msg_id;
void updateChartState();
void expandSignal(const Signal *sig);
void rowClicked(const QModelIndex &index);
SignalModel *model = nullptr;
signals:
void highlight(const Signal *sig);
void showChart(const QString &name, const Signal *sig, bool show, bool merge);
void remove(const Signal *sig);
void save(const Signal *sig, const Signal &new_sig);
void showFormClicked(const Signal *sig);
protected:
void enterEvent(QEvent *event) override;
void leaveEvent(QEvent *event) override;
void saveSignal();
ElidedLabel *title;
QLabel *color_label;
QLabel *icon;
int form_idx = 0;
QColor bg_color;
QToolButton *plot_btn;
private:
void rowsChanged();
QString msg_id;
QTreeView *tree;
QLineEdit *filter_edit;
ChartsWidget *charts;
QLabel *signal_count_lb;
};

Loading…
Cancel
Save