#include "tools/cabana/messageswidget.h" #include #include #include #include #include #include #include #include "tools/cabana/commands.h" static QString msg_node_from_id(const MessageId &id) { auto msg = dbc()->msg(id); return msg ? msg->transmitter : QString(); } MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setSpacing(0); // toolbar main_layout->addWidget(createToolBar()); // message table view = new MessageView(this); model = new MessageListModel(this); header = new MessageViewHeader(this); view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes)); view->setHeader(header); view->setModel(model); 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); header->setSectionsMovable(true); header->setSectionResizeMode(MessageListModel::Column::DATA, QHeaderView::Fixed); header->setStretchLastSection(true); header->setContextMenuPolicy(Qt::CustomContextMenu); 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(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); QObject::connect(header, &MessageViewHeader::filtersUpdated, model, &MessageListModel::setFilterStrings); QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); 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 )")); } QToolBar *MessagesWidget::createToolBar() { QToolBar *toolbar = new QToolBar(this); toolbar->setIconSize({12, 12}); toolbar->addWidget(num_msg_label = new QLabel(this)); num_msg_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); auto views_btn = toolbar->addAction(utils::icon("three-dots"), tr("View...")); views_btn->setMenu(menu); auto view_button = qobject_cast(toolbar->widgetForAction(views_btn)); view_button->setPopupMode(QToolButton::InstantPopup); view_button->setToolButtonStyle(Qt::ToolButtonIconOnly); view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }"); return toolbar; } 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())); } } void MessagesWidget::headerContextMenuEvent(const QPoint &pos) { menu->exec(header->mapToGlobal(pos)); } void MessagesWidget::menuAboutToShow() { menu->clear(); for (int i = 0; i < header->count(); ++i) { int logical_index = header->logicalIndex(i); auto action = menu->addAction(model->headerData(logical_index, Qt::Horizontal).toString(), [=](bool checked) { header->setSectionHidden(logical_index, !checked); }); action->setCheckable(true); action->setChecked(!header->isSectionHidden(logical_index)); // Can't hide the name column action->setEnabled(logical_index > 0); } menu->addSeparator(); auto action = menu->addAction(tr("Mutlti-Line bytes"), this, &MessagesWidget::setMultiLineBytes); action->setCheckable(true); action->setChecked(settings.multiple_lines_bytes); } void MessagesWidget::setMultiLineBytes(bool multi) { settings.multiple_lines_bytes = multi; delegate->setMultipleLines(multi); view->updateBytesSectionSize(); view->doItemsLayout(); } // 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::NODE: return tr("Node"); 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 { if (!index.isValid() || index.row() >= msgs.size()) return {}; auto getFreq = [](const CanData &d) { if (d.freq > 0 && (can->currentSec() - d.ts - 1.0 / settings.fps) < (5.0 / d.freq)) { return d.freq >= 0.95 ? QString::number(std::nearbyint(d.freq)) : QString::number(d.freq, 'f', 2); } else { return QStringLiteral("--"); } }; const auto &id = msgs[index.row()]; auto &can_data = can->lastMessage(id); 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::NODE: return msg_node_from_id(id); 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; filterAndSort(); } void MessageListModel::dbcModified() { dbc_address.clear(); for (const auto &[_, m] : dbc()->getMessages(-1)) { dbc_address.insert(m.address); } filterAndSort(); } void MessageListModel::sortMessages(std::vector &new_msgs) { auto do_sort = [order = sort_order](std::vector &m, auto proj) { std::sort(m.begin(), m.end(), [order, proj = std::move(proj)](auto &l, auto &r) { return order == Qt::AscendingOrder ? proj(l) < proj(r) : proj(l) > proj(r); }); }; switch (sort_column) { case Column::NAME: do_sort(new_msgs, [](auto &id) { return std::make_pair(msgName(id), id); }); break; case Column::SOURCE: do_sort(new_msgs, [](auto &id) { return std::tie(id.source, id); }); break; case Column::ADDRESS: do_sort(new_msgs, [](auto &id) { return std::tie(id.address, id);}); break; case Column::NODE: do_sort(new_msgs, [](auto &id) { return std::make_pair(msg_node_from_id(id), id);}); break; case Column::FREQ: do_sort(new_msgs, [](auto &id) { return std::tie(can->lastMessage(id).freq, id); }); break; case Column::COUNT: do_sort(new_msgs, [](auto &id) { return std::tie(can->lastMessage(id).count, id); }); break; } } 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 = 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 = match || parseRange(txt, id.address, 16); break; } case Column::NODE: match = re.match(msg_node_from_id(id)).hasMatch(); 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 = match || re.match(QString(data.dat.toHex())).hasMatch(); match = match || re.match(QString(data.dat.toHex(' '))).hasMatch(); break; } } } return match; } void MessageListModel::filterAndSort() { 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)) { filterAndSort(); } 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; filterAndSort(); } } 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(); } // 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); } } // MessageViewHeader 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] && !editors[i]->text().isEmpty()) { filters[i] = editors[i]->text(); } } emit filtersUpdated(filters); } 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(); return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz; }