#include "tools/cabana/messageswidget.h" #include #include #include #include #include #include "tools/cabana/commands.h" MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0 ,0, 0, 0); QHBoxLayout *title_layout = new QHBoxLayout(); num_msg_label = new QLabel(this); title_layout->addSpacing(10); title_layout->addWidget(num_msg_label); title_layout->addStretch(); title_layout->addWidget(multiple_lines_bytes = new QCheckBox(tr("Multiple Lines &Bytes"), this)); multiple_lines_bytes->setToolTip(tr("Display bytes in multiple lines")); multiple_lines_bytes->setChecked(settings.multiple_lines_bytes); QPushButton *clear_filters = new QPushButton(tr("&Clear Filters")); clear_filters->setEnabled(false); title_layout->addWidget(clear_filters); main_layout->addLayout(title_layout); // message table view = new MessageView(this); model = new MessageListModel(this); header = new MessageViewHeader(this); auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); view->setItemDelegate(delegate); view->setHeader(header); view->setModel(model); view->setHeader(header); view->setSortingEnabled(true); view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder); view->setAllColumnsShowFocus(true); view->setEditTriggers(QAbstractItemView::NoEditTriggers); view->setItemsExpandable(false); view->setIndentation(0); view->setRootIsDecorated(false); // Must be called before setting any header parameters to avoid overriding restoreHeaderState(settings.message_header_state); view->header()->setSectionsMovable(true); view->header()->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); view->header()->setStretchLastSection(true); // Header context menu view->header()->setContextMenuPolicy(Qt::CustomContextMenu); QObject::connect(view->header(), &QHeaderView::customContextMenuRequested, view, &MessageView::headerContextMenuEvent); main_layout->addWidget(view); // suppress QHBoxLayout *suppress_layout = new QHBoxLayout(); suppress_add = new QPushButton("Suppress Highlighted"); suppress_clear = new QPushButton(); suppress_layout->addWidget(suppress_add); suppress_layout->addWidget(suppress_clear); QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Defined Signals"), this); suppress_defined_signals->setChecked(settings.suppress_defined_signals); suppress_layout->addWidget(suppress_defined_signals); main_layout->addLayout(suppress_layout); // signals/slots QObject::connect(header, &MessageViewHeader::filtersUpdated, model, &MessageListModel::setFilterStrings); QObject::connect(header, &MessageViewHeader::filtersUpdated, [=](const QMap &filters) { clear_filters->setEnabled(!filters.isEmpty()); }); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); QObject::connect(clear_filters, &QPushButton::clicked, header, &MessageViewHeader::clearFilters); QObject::connect(multiple_lines_bytes, &QCheckBox::stateChanged, [=](int state) { settings.multiple_lines_bytes = (state == Qt::Checked); delegate->setMultipleLines(settings.multiple_lines_bytes); view->setUniformRowHeights(!settings.multiple_lines_bytes); // Reset model to force recalculation of the width of the bytes column model->forceResetModel(); }); QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, [=](int state) { settings.suppress_defined_signals = (state == Qt::Checked); emit settings.changed(); }); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MessagesWidget::dbcModified); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MessagesWidget::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); } view->updateBytesSectionSize(); }); QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { if (current.isValid() && current.row() < model->msgs.size()) { auto &id = model->msgs[current.row()]; if (!current_msg_id || id != *current_msg_id) { current_msg_id = id; emit msgSelectionChanged(*current_msg_id); } } }); QObject::connect(suppress_add, &QPushButton::clicked, [=]() { model->suppress(); updateSuppressedButtons(); }); QObject::connect(suppress_clear, &QPushButton::clicked, [=]() { model->clearSuppress(); updateSuppressedButtons(); }); updateSuppressedButtons(); setWhatsThis(tr(R"( Message View
Byte color
constant changing
increasing
decreasing )")); } void MessagesWidget::dbcModified() { num_msg_label->setText(tr("%1 Messages, %2 Signals").arg(dbc()->msgCount()).arg(dbc()->signalCount())); model->dbcModified(); } void MessagesWidget::selectMessage(const MessageId &msg_id) { auto it = std::find(model->msgs.cbegin(), model->msgs.cend(), msg_id); if (it != model->msgs.cend()) { view->setCurrentIndex(model->index(std::distance(model->msgs.cbegin(), it), 0)); } } void MessagesWidget::updateSuppressedButtons() { if (model->suppressed_bytes.empty()) { suppress_clear->setEnabled(false); suppress_clear->setText("Clear Suppressed"); } else { suppress_clear->setEnabled(true); suppress_clear->setText(QString("Clear Suppressed (%1)").arg(model->suppressed_bytes.size())); } } // MessageListModel QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { case Column::NAME: return tr("Name"); case Column::SOURCE: return tr("Bus"); case Column::ADDRESS: return tr("ID"); case Column::FREQ: return tr("Freq"); case Column::COUNT: return tr("Count"); case Column::DATA: return tr("Bytes"); } } return {}; } QVariant MessageListModel::data(const QModelIndex &index, int role) const { const auto &id = msgs[index.row()]; auto &can_data = can->lastMessage(id); auto getFreq = [](const CanData &d) -> QString { if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) { return d.freq >= 1 ? QString::number(std::nearbyint(d.freq)) : QString::number(d.freq, 'f', 2); } else { return "--"; } }; if (role == Qt::DisplayRole) { switch (index.column()) { case Column::NAME: return msgName(id); case Column::SOURCE: return id.source != INVALID_SOURCE ? QString::number(id.source) : "N/A" ; case Column::ADDRESS: return QString::number(id.address, 16); case Column::FREQ: return id.source != INVALID_SOURCE ? getFreq(can_data) : "N/A"; case Column::COUNT: return id.source != INVALID_SOURCE ? QString::number(can_data.count) : "N/A"; case Column::DATA: return id.source != INVALID_SOURCE ? toHex(can_data.dat) : "N/A"; } } else if (role == ColorsRole) { QVector colors = can_data.colors; if (!suppressed_bytes.empty()) { for (int i = 0; i < colors.size(); i++) { if (suppressed_bytes.contains({id, i})) { colors[i] = QColor(255, 255, 255, 0); } } } return QVariant::fromValue(colors); } else if (role == BytesRole && index.column() == Column::DATA && id.source != INVALID_SOURCE) { return can_data.dat; } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { auto msg = dbc()->msg(id); auto tooltip = msg ? msg->name : UNTITLED; if (msg && !msg->comment.isEmpty()) tooltip += "
" + msg->comment + ""; return tooltip; } return {}; } void MessageListModel::setFilterStrings(const QMap &filters) { filter_str = filters; fetchData(); } void MessageListModel::dbcModified() { dbc_address.clear(); for (const auto &[_, m] : dbc()->getMessages(-1)) { dbc_address.insert(m.address); } fetchData(); } void MessageListModel::sortMessages(std::vector &new_msgs) { if (sort_column == Column::NAME) { std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { auto ll = std::pair{msgName(l), l}; auto rr = std::pair{msgName(r), r}; return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; }); } else if (sort_column == Column::SOURCE) { std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { auto ll = std::pair{l.source, l}; auto rr = std::pair{r.source, r}; return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; }); } else if (sort_column == Column::ADDRESS) { std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { auto ll = std::pair{l.address, l}; auto rr = std::pair{r.address, r}; return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; }); } else if (sort_column == Column::FREQ) { std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { auto ll = std::pair{can->lastMessage(l).freq, l}; auto rr = std::pair{can->lastMessage(r).freq, r}; return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; }); } else if (sort_column == Column::COUNT) { std::sort(new_msgs.begin(), new_msgs.end(), [=](auto &l, auto &r) { auto ll = std::pair{can->lastMessage(l).count, l}; auto rr = std::pair{can->lastMessage(r).count, r}; return sort_order == Qt::AscendingOrder ? ll < rr : ll > rr; }); } } static bool parseRange(const QString &filter, uint32_t value, int base = 10) { // Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf}) unsigned int min = std::numeric_limits::min(); unsigned int max = std::numeric_limits::max(); auto s = filter.split('-'); bool ok = s.size() >= 1 && s.size() <= 2; if (ok && !s[0].isEmpty()) min = s[0].toUInt(&ok, base); if (ok && s.size() == 1) { max = min; } else if (ok && s.size() == 2 && !s[1].isEmpty()) { max = s[1].toUInt(&ok, base); } return ok && value >= min && value <= max; } bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, const QMap &filters) { bool match = true; for (auto it = filters.cbegin(); it != filters.cend() && match; ++it) { const QString &txt = it.value(); QRegularExpression re(txt, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); switch (it.key()) { case Column::NAME: { const auto msg = dbc()->msg(id); match = re.match(msg ? msg->name : UNTITLED).hasMatch(); match |= msg && std::any_of(msg->sigs.cbegin(), msg->sigs.cend(), [&re](const auto &s) { return re.match(s->name).hasMatch(); }); break; } case Column::SOURCE: match = parseRange(txt, id.source); break; case Column::ADDRESS: { match = re.match(QString::number(id.address, 16)).hasMatch(); match |= parseRange(txt, id.address, 16); break; } case Column::FREQ: // TODO: Hide stale messages? match = parseRange(txt, data.freq); break; case Column::COUNT: match = parseRange(txt, data.count); break; case Column::DATA: { match = QString(data.dat.toHex()).contains(txt, Qt::CaseInsensitive); match |= re.match(QString(data.dat.toHex())).hasMatch(); match |= re.match(QString(data.dat.toHex(' '))).hasMatch(); break; } } } return match; } void MessageListModel::fetchData() { std::vector new_msgs; new_msgs.reserve(can->last_msgs.size() + dbc_address.size()); auto address = dbc_address; for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { if (filter_str.isEmpty() || matchMessage(it.key(), it.value(), filter_str)) { new_msgs.push_back(it.key()); } address.remove(it.key().address); } // merge all DBC messages for (auto &addr : address) { MessageId id{.source = INVALID_SOURCE, .address = addr}; if (filter_str.isEmpty() || matchMessage(id, {}, filter_str)) { new_msgs.push_back(id); } } sortMessages(new_msgs); if (msgs != new_msgs) { beginResetModel(); msgs = std::move(new_msgs); endResetModel(); } } void MessageListModel::msgsReceived(const QHash *new_msgs, bool has_new_ids) { if (has_new_ids || filter_str.contains(Column::FREQ) || filter_str.contains(Column::COUNT) || filter_str.contains(Column::DATA)) { fetchData(); } for (int i = 0; i < msgs.size(); ++i) { if (new_msgs->contains(msgs[i])) { for (int col = Column::FREQ; col < columnCount(); ++col) emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole}); } } } void MessageListModel::sort(int column, Qt::SortOrder order) { if (column != columnCount() - 1) { sort_column = column; sort_order = order; fetchData(); } } void MessageListModel::suppress() { const double cur_ts = can->currentSec(); for (auto &id : msgs) { auto &can_data = can->lastMessage(id); for (int i = 0; i < can_data.dat.size(); i++) { const double dt = cur_ts - can_data.last_change_t[i]; if (dt < 2.0) { suppressed_bytes.insert({id, i}); } } } } void MessageListModel::clearSuppress() { suppressed_bytes.clear(); } void MessageListModel::forceResetModel() { beginResetModel(); endResetModel(); } // MessageView void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QTreeView::drawRow(painter, option, index); const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); const QColor gridColor = QColor::fromRgba(static_cast(gridHint)); QPen old_pen = painter->pen(); painter->setPen(gridColor); painter->drawLine(option.rect.left(), option.rect.bottom(), option.rect.right(), option.rect.bottom()); auto y = option.rect.y(); painter->translate(visualRect(model()->index(0, 0)).x() - indentation() - .5, -.5); for (int i = 0; i < header()->count(); ++i) { painter->translate(header()->sectionSize(header()->logicalIndex(i)), 0); painter->drawLine(0, y, 0, y + option.rect.height()); } painter->setPen(old_pen); painter->resetTransform(); } void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { // Bypass the slow call to QTreeView::dataChanged. // QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView. QAbstractItemView::dataChanged(topLeft, bottomRight, roles); } void MessageView::updateBytesSectionSize() { auto delegate = ((MessageBytesDelegate *)itemDelegate()); int max_bytes = 8; if (!delegate->multipleLines()) { for (auto it = can->last_msgs.constBegin(); it != can->last_msgs.constEnd(); ++it) { max_bytes = std::max(max_bytes, it.value().dat.size()); } } int width = delegate->widthForBytes(max_bytes); if (header()->sectionSize(MessageListModel::Column::DATA) != width) { header()->resizeSection(MessageListModel::Column::DATA, width); } } void MessageView::headerContextMenuEvent(const QPoint &pos) { QMenu *menu = new QMenu(this); int cur_index = header()->logicalIndexAt(pos); QAction *action; for (int visual_index = 0; visual_index < header()->count(); visual_index++) { int logical_index = header()->logicalIndex(visual_index); QString column_name = model()->headerData(logical_index, Qt::Horizontal).toString(); // Hide show action if (header()->isSectionHidden(logical_index)) { action = menu->addAction(tr("  %1").arg(column_name), [=]() { header()->showSection(logical_index); }); } else { action = menu->addAction(tr("✓ %1").arg(column_name), [=]() { header()->hideSection(logical_index); }); } // Can't hide the name column action->setEnabled(logical_index > 0); // Make current column bold if (logical_index == cur_index) { QFont font = action->font(); font.setBold(true); action->setFont(font); } } menu->popup(header()->mapToGlobal(pos)); } MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizontal, parent) { QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions); QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions); } void MessageViewHeader::updateFilters() { QMap filters; for (int i = 0; i < count(); i++) { if (editors[i]) { QString filter = editors[i]->text(); if (!filter.isEmpty()) { filters[i] = filter; } } } emit filtersUpdated(filters); } void MessageViewHeader::clearFilters() { for (QLineEdit *editor : editors) { editor->clear(); } } void MessageViewHeader::updateHeaderPositions() { QSize sz = QHeaderView::sizeHint(); for (int i = 0; i < count(); i++) { if (editors[i]) { int h = editors[i]->sizeHint().height(); editors[i]->move(sectionViewportPosition(i), sz.height()); editors[i]->resize(sectionSize(i), h); editors[i]->setHidden(isSectionHidden(i)); } } } void MessageViewHeader::updateGeometries() { for (int i = 0; i < count(); i++) { if (!editors[i]) { QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); editors[i] = new QLineEdit(this); editors[i]->setClearButtonEnabled(true); editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name)); QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters); } } setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0); QHeaderView::updateGeometries(); updateHeaderPositions(); } QSize MessageViewHeader::sizeHint() const { QSize sz = QHeaderView::sizeHint(); if (editors[0]) { sz.setHeight(sz.height() + editors[0]->minimumSizeHint().height() + 1); } return sz; }