#include "tools/cabana/messageswidget.h" #include #include #include #include #include #include #include #include #include #include "tools/cabana/commands.h" MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0, 0, 0, 0); // toolbar main_layout->addWidget(createToolBar()); // message table main_layout->addWidget(view = new MessageView(this)); view->setItemDelegate(delegate = new MessageBytesDelegate(view, settings.multiple_lines_hex)); view->setModel(model = new MessageListModel(this)); view->setHeader(header = new MessageViewHeader(this)); 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); // signals/slots QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::dbcModified); QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); } view->updateBytesSectionSize(); updateTitle(); }); QObject::connect(view->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { if (current.isValid() && current.row() < model->items_.size()) { const auto &id = model->items_[current.row()].id; if (!current_msg_id || id != *current_msg_id) { current_msg_id = id; emit msgSelectionChanged(*current_msg_id); } } }); setWhatsThis(tr(R"( Message View
Byte color
constant changing
increasing
decreasing
Shortcuts
Horizontal Scrolling:  shift+wheel  )")); } QWidget *MessagesWidget::createToolBar() { QWidget *toolbar = new QWidget(this); QHBoxLayout *layout = new QHBoxLayout(toolbar); layout->setContentsMargins(0, 9, 0, 0); layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted")); layout->addWidget(suppress_clear = new QPushButton()); suppress_clear->setToolTip(tr("Clear suppressed")); layout->addStretch(1); QCheckBox *suppress_defined_signals = new QCheckBox(tr("Suppress Signals"), this); suppress_defined_signals->setToolTip(tr("Suppress defined signals")); suppress_defined_signals->setChecked(settings.suppress_defined_signals); layout->addWidget(suppress_defined_signals); auto view_button = new ToolButton("three-dots", tr("View...")); view_button->setMenu(menu); view_button->setPopupMode(QToolButton::InstantPopup); view_button->setStyleSheet("QToolButton::menu-indicator { image: none; }"); layout->addWidget(view_button); QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals); suppressHighlighted(); return toolbar; } void MessagesWidget::updateTitle() { auto stats = std::accumulate( model->items_.begin(), model->items_.end(), std::pair(), [](const auto &pair, const auto &item) { auto m = dbc()->msg(item.id); return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair; }); emit titleChanged(tr("%1 Messages (%2 DBC Messages, %3 Signals)") .arg(model->items_.size()).arg(stats.first).arg(stats.second)); } void MessagesWidget::selectMessage(const MessageId &msg_id) { auto it = std::find_if(model->items_.cbegin(), model->items_.cend(), [&msg_id](auto &item) { return item.id == msg_id; }); if (it != model->items_.cend()) { view->setCurrentIndex(model->index(std::distance(model->items_.cbegin(), it), 0)); } } void MessagesWidget::suppressHighlighted() { int n = sender() == suppress_add ? can->suppressHighlighted() : (can->clearSuppressed(), 0); suppress_clear->setText(n > 0 ? tr("Clear (%1)").arg(n) : tr("Clear")); suppress_clear->setEnabled(n > 0); } 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("Multi-Line bytes"), this, &MessagesWidget::setMultiLineBytes); action->setCheckable(true); action->setChecked(settings.multiple_lines_hex); action = menu->addAction(tr("Show inactive Messages"), model, &MessageListModel::showInactivemessages); action->setCheckable(true); action->setChecked(model->show_inactive_messages); } void MessagesWidget::setMultiLineBytes(bool multi) { settings.multiple_lines_hex = 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() >= items_.size()) return {}; auto getFreq = [](float freq) { if (freq > 0) { return freq >= 0.95 ? QString::number(std::nearbyint(freq)) : QString::number(freq, 'f', 2); } else { return QStringLiteral("--"); } }; const static QString NA = QStringLiteral("N/A"); const auto &item = items_[index.row()]; if (role == Qt::DisplayRole) { switch (index.column()) { case Column::NAME: return item.name; case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; case Column::ADDRESS: return toHexString(item.id.address); case Column::NODE: return item.node; case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : NA; } } else if (role == ColorsRole) { return QVariant::fromValue((void*)(&can->lastMessage(item.id).colors)); } else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) { return QVariant::fromValue((void*)(&can->lastMessage(item.id).dat)); } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { auto msg = dbc()->msg(item.id); auto tooltip = item.name; if (msg && !msg->comment.isEmpty()) tooltip += "
" + msg->comment + ""; return tooltip; } return {}; } void MessageListModel::setFilterStrings(const QMap &filters) { filters_ = filters; filterAndSort(); } void MessageListModel::showInactivemessages(bool show) { show_inactive_messages = show; filterAndSort(); } void MessageListModel::dbcModified() { dbc_messages_.clear(); for (const auto &[_, m] : dbc()->getMessages(-1)) { dbc_messages_.insert(MessageId{.source = INVALID_SOURCE, .address = m.address}); } filterAndSort(); } void MessageListModel::sortItems(std::vector &items) { auto compare = [this](const auto &l, const auto &r) { switch (sort_column) { case Column::NAME: return std::tie(l.name, l.id) < std::tie(r.name, r.id); case Column::SOURCE: return std::tie(l.id.source, l.id.address) < std::tie(r.id.source, r.id.address); case Column::ADDRESS: return std::tie(l.id.address, l.id.source) < std::tie(r.id.address, r.id.source); case Column::NODE: return std::tie(l.node, l.id) < std::tie(r.node, r.id); case Column::FREQ: return std::tie(can->lastMessage(l.id).freq, l.id) < std::tie(can->lastMessage(r.id).freq, r.id); case Column::COUNT: return std::tie(can->lastMessage(l.id).count, l.id) < std::tie(can->lastMessage(r.id).count, r.id); default: return false; // Default case to suppress compiler warning } }; if (sort_order == Qt::DescendingOrder) std::stable_sort(items.rbegin(), items.rend(), compare); else std::stable_sort(items.begin(), items.end(), compare); } 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::match(const MessageListModel::Item &item) { if (filters_.isEmpty()) return true; bool match = true; const auto &data = can->lastMessage(item.id); for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) { const QString &txt = it.value(); switch (it.key()) { case Column::NAME: { match = item.name.contains(txt, Qt::CaseInsensitive); if (!match) { const auto m = dbc()->msg(item.id); match = m && std::any_of(m->sigs.cbegin(), m->sigs.cend(), [&txt](const auto &s) { return s->name.contains(txt, Qt::CaseInsensitive); }); } break; } case Column::SOURCE: match = parseRange(txt, item.id.source); break; case Column::ADDRESS: match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive); match = match || parseRange(txt, item.id.address, 16); break; case Column::NODE: match = item.node.contains(txt, Qt::CaseInsensitive); break; case Column::FREQ: match = parseRange(txt, data.freq); break; case Column::COUNT: match = parseRange(txt, data.count); break; case Column::DATA: match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive); break; } } return match; } bool MessageListModel::filterAndSort() { // merge CAN and DBC messages std::vector all_messages; all_messages.reserve(can->lastMessages().size() + dbc_messages_.size()); auto dbc_msgs = dbc_messages_; for (const auto &[id, m] : can->lastMessages()) { all_messages.push_back(id); dbc_msgs.erase(MessageId{.source = INVALID_SOURCE, .address = id.address}); } all_messages.insert(all_messages.end(), dbc_msgs.begin(), dbc_msgs.end()); // filter and sort std::vector items; items.reserve(all_messages.size()); for (const auto &id : all_messages) { if (show_inactive_messages || can->isMessageActive(id)) { auto msg = dbc()->msg(id); Item item = {.id = id, .name = msg ? msg->name : UNTITLED, .node = msg ? msg->transmitter : QString()}; if (match(item)) items.emplace_back(item); } } sortItems(items); if (items_ != items) { beginResetModel(); items_ = std::move(items); endResetModel(); return true; } return false; } void MessageListModel::msgsReceived(const std::set *new_msgs, bool has_new_ids) { if (has_new_ids || ((filters_.count(Column::FREQ) || filters_.count(Column::COUNT) || filters_.count(Column::DATA)) && ++sort_threshold_ == settings.fps)) { sort_threshold_ = 0; if (filterAndSort()) return; } // Update viewport emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); } void MessageListModel::sort(int column, Qt::SortOrder order) { if (column != Column::DATA) { sort_column = column; sort_order = order; filterAndSort(); } } // MessageView void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { const auto &item = ((MessageListModel*)model())->items_[index.row()]; if (!can->isMessageActive(item.id)) { QStyleOptionViewItem custom_option = option; custom_option.palette.setBrush(QPalette::Text, custom_option.palette.color(QPalette::Disabled, QPalette::Text)); auto color = QApplication::palette().color(QPalette::HighlightedText); color.setAlpha(100); custom_option.palette.setBrush(QPalette::HighlightedText, color); QTreeView::drawRow(painter, custom_option, index); } else { QTreeView::drawRow(painter, option, index); } QPen oldPen = painter->pen(); const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); painter->setPen(QColor::fromRgba(static_cast(gridHint))); // Draw bottom border for the row painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight()); // Draw vertical borders for each column for (int i = 0; i < header()->count(); ++i) { int sectionX = header()->sectionViewportPosition(i); painter->drawLine(sectionX, option.rect.top(), sectionX, option.rect.bottom()); } painter->setPen(oldPen); } 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 (const auto &[_, m] : can->lastMessages()) { max_bytes = std::max(max_bytes, m.dat.size()); } } setUniformRowHeights(!delegate->multipleLines()); header()->resizeSection(MessageListModel::Column::DATA, delegate->sizeForBytes(max_bytes).width()); } void MessageView::wheelEvent(QWheelEvent *event) { if (event->modifiers() == Qt::ShiftModifier) { QApplication::sendEvent(horizontalScrollBar(), event); } else { QTreeView::wheelEvent(event); } } // 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(); } } qobject_cast(model())->setFilterStrings(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]->setGeometry(sectionViewportPosition(i), sz.height(), 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; }