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.
461 lines
17 KiB
461 lines
17 KiB
#include "tools/cabana/signaledit.h"
|
|
|
|
#include <QGuiApplication>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QToolButton>
|
|
#include <QVBoxLayout>
|
|
|
|
#include "tools/cabana/commands.h"
|
|
|
|
// SignalModel
|
|
|
|
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);
|
|
}
|
|
|
|
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 SignalModel::setMessage(const MessageId &id) {
|
|
msg_id = id;
|
|
filter_str = "";
|
|
refresh();
|
|
updateState(nullptr);
|
|
}
|
|
|
|
void SignalModel::setFilter(const QString &txt) {
|
|
filter_str = txt;
|
|
refresh();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void SignalModel::updateState(const QHash<MessageId, 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
int SignalModel::rowCount(const QModelIndex &parent) const {
|
|
if (parent.column() > 0) return 0;
|
|
|
|
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;
|
|
}
|
|
|
|
Qt::ItemFlags SignalModel::flags(const QModelIndex &index) const {
|
|
if (!index.isValid()) return Qt::NoItemFlags;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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 utils::icon(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 (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);
|
|
}
|
|
}
|
|
if (s.is_little_endian) {
|
|
s.lsb = s.start_bit;
|
|
s.msb = s.start_bit + s.size - 1;
|
|
} else {
|
|
s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1);
|
|
s.msb = s.start_bit;
|
|
}
|
|
|
|
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 == msg_id.address) {
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
void SignalModel::handleSignalAdded(uint32_t address, const Signal *sig) {
|
|
if (address == msg_id.address) {
|
|
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);
|
|
small_font.setPointSize(8);
|
|
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 = getColor(item->sig);
|
|
QRect rc{option.rect.left(), option.rect.top(), 18, 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), 3, 3);
|
|
painter->setPen(item->highlight ? Qt::white : Qt::black);
|
|
painter->setFont(small_font);
|
|
painter->drawText(rc, Qt::AlignCenter, QString::number(item->row() + 1));
|
|
|
|
// signal name
|
|
painter->setFont(option.font);
|
|
painter->setPen((option.state & QStyle::State_Selected ? option.palette.highlightedText() : option.palette.text()).color());
|
|
QString text = index.data(Qt::DisplayRole).toString();
|
|
QRect text_rect = option.rect.adjusted(rc.width() + 6, 0, 0, 0);
|
|
text = painter->fontMetrics().elidedText(text, Qt::ElideRight, text_rect.width());
|
|
painter->drawText(text_rect, option.displayAlignment, text);
|
|
painter->restore();
|
|
} else {
|
|
QStyledItemDelegate::paint(painter, option, index);
|
|
}
|
|
}
|
|
|
|
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(utils::icon("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); });
|
|
|
|
setWhatsThis(tr(R"(
|
|
<b>Signal view</b><br />
|
|
<!-- TODO: add descprition here -->
|
|
)"));
|
|
}
|
|
|
|
void SignalView::setMessage(const MessageId &id) {
|
|
msg_id = id;
|
|
filter_edit->clear();
|
|
model->setMessage(id);
|
|
}
|
|
|
|
void SignalView::rowsChanged() {
|
|
auto create_btn = [](const QString &id, const QString &tooltip) {
|
|
auto btn = new QToolButton();
|
|
btn->setIcon(utils::icon(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);
|
|
});
|
|
}
|
|
}
|
|
updateChartState();
|
|
}
|
|
|
|
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 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 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
void SignalView::leaveEvent(QEvent *event) {
|
|
emit highlight(nullptr);
|
|
QWidget::leaveEvent(event);
|
|
}
|
|
|