diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 66523333ad..f43185e025 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,42 +1,46 @@ #include "tools/cabana/messageswidget.h" - #include #include #include +#include #include MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(0 ,0, 0, 0); - // message filter QHBoxLayout *title_layout = new QHBoxLayout(); - title_layout->addWidget(filter = new QLineEdit(this)); - QRegularExpression re("\\S+"); - filter->setValidator(new QRegularExpressionValidator(re, this)); - filter->setClearButtonEnabled(true); - filter->setPlaceholderText(tr("filter messages")); + 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")); + title_layout->addWidget(clear_filters); main_layout->addLayout(title_layout); // message table view = new MessageView(this); model = new MessageListModel(this); + header = new MessageViewHeader(this, model); auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); + view->setItemDelegate(delegate); view->setModel(model); view->setSortingEnabled(true); - view->sortByColumn(0, Qt::AscendingOrder); + view->sortByColumn(MessageListModel::Column::NAME, Qt::AscendingOrder); view->setAllColumnsShowFocus(true); view->setEditTriggers(QAbstractItemView::NoEditTriggers); view->setItemsExpandable(false); view->setIndentation(0); view->setRootIsDecorated(false); + view->setHeader(header); + // Must be called before setting any header parameters to avoid overriding restoreHeaderState(settings.message_header_state); - view->header()->setSectionsMovable(true); // Header context menu @@ -57,21 +61,28 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { main_layout->addLayout(suppress_layout); // signals/slots - QObject::connect(filter, &QLineEdit::textEdited, model, &MessageListModel::setFilterString); + QObject::connect(header, &MessageViewHeader::filtersUpdated, model, &MessageListModel::setFilterStrings); + 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); - model->sortMessages(); + + // 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); }); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(can, &AbstractStream::streamStarted, this, &MessagesWidget::reset); - QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::sortMessages); - QObject::connect(dbc(), &DBCManager::msgUpdated, model, &MessageListModel::sortMessages); - QObject::connect(dbc(), &DBCManager::msgRemoved, model, &MessageListModel::sortMessages); + QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::msgUpdated, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::msgRemoved, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::signalAdded, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::signalRemoved, this, &MessagesWidget::dbcModified); + QObject::connect(dbc(), &DBCManager::signalUpdated, this, &MessagesWidget::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -97,6 +108,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { }); updateSuppressedButtons(); + dbcModified(); setWhatsThis(tr(R"( Message View
@@ -108,6 +120,11 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { )")); } +void MessagesWidget::dbcModified() { + num_msg_label->setText(tr("%1 Messages, %2 Signals").arg(dbc()->msgCount()).arg(dbc()->signalCount())); + model->fetchData(); +} + void MessagesWidget::selectMessage(const MessageId &msg_id) { if (int row = model->msgs.indexOf(msg_id); row != -1) { view->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); @@ -128,7 +145,6 @@ void MessagesWidget::reset() { current_msg_id = std::nullopt; view->selectionModel()->clear(); model->reset(); - filter->clear(); updateSuppressedButtons(); } @@ -137,8 +153,14 @@ void MessagesWidget::reset() { QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { - static const QString titles[] = {"Name", "Bus", "ID", "Freq", "Count", "Bytes"}; - return titles[section]; + 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 {}; } @@ -157,12 +179,12 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { switch (index.column()) { - case 0: return msgName(id); - case 1: return id.source; - case 2: return QString::number(id.address, 16); - case 3: return getFreq(can_data); - case 4: return can_data.count; - case 5: return toHex(can_data.dat); + case Column::NAME: return msgName(id); + case Column::SOURCE: return id.source; + case Column::ADDRESS: return QString::number(id.address, 16); + case Column::FREQ: return getFreq(can_data); + case Column::COUNT: return can_data.count; + case Column::DATA: return toHex(can_data.dat); } } else if (role == ColorsRole) { QVector colors = can_data.colors; @@ -174,83 +196,173 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { } } return QVariant::fromValue(colors); - } else if (role == BytesRole && index.column() == 5) { + } else if (role == BytesRole && index.column() == Column::DATA) { return can_data.dat; } return {}; } -void MessageListModel::setFilterString(const QString &string) { - auto contains = [](const MessageId &id, const QString &txt) { - auto cs = Qt::CaseInsensitive; - if (id.toString().contains(txt, cs) || msgName(id).contains(txt, cs)) return true; - // Search by signal name - if (const auto msg = dbc()->msg(id)) { - for (auto s : msg->getSignals()) { - if (s->name.contains(txt, cs)) return true; - } - } - return false; - }; - - filter_str = string; - msgs.clear(); - for (auto it = can->last_msgs.begin(); it != can->last_msgs.end(); ++it) { - if (filter_str.isEmpty() || contains(it.key(), filter_str)) { - msgs.push_back(it.key()); - } - } - sortMessages(); +void MessageListModel::setFilterStrings(const QMap &filters) { + filter_str = filters; + fetchData(); } -void MessageListModel::sortMessages() { - beginResetModel(); - if (sort_column == 0) { - std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { +void MessageListModel::sortMessages(Qt::SortOrder sort_order, int sort_column, QList &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 == 1) { - std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { + } 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 == 2) { - std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { + } 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 == 3) { - std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { + } 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 == 4) { - std::sort(msgs.begin(), msgs.end(), [this](auto &l, auto &r) { + } 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; }); } - endResetModel(); } -void MessageListModel::msgsReceived(const QHash *new_msgs) { - int prev_row_count = msgs.size(); - if (filter_str.isEmpty() && msgs.size() != can->last_msgs.size()) { - msgs = can->last_msgs.keys(); +static std::pair parseRange(QString &filter, bool *ok = nullptr, int base = 10) { + // Parse out filter string into a range (e.g. "1" -> {1, 1}, "1-3" -> {1, 3}, "1-" -> {1, inf}) + bool ok1 = true, ok2 = true; + unsigned int parsed1 = std::numeric_limits::min(); + unsigned int parsed2 = std::numeric_limits::max(); + + auto s = filter.split('-'); + if (s.size() == 1) { + parsed1 = s[0].toUInt(ok, base); + return {parsed1, parsed1}; + } else if (s.size() == 2) { + if (!s[0].isEmpty()) parsed1 = s[0].toUInt(&ok1, base); + if (!s[1].isEmpty()) parsed2 = s[1].toUInt(&ok2, base); + + *ok = ok1 & ok2; + return {parsed1, parsed2}; + } else { + *ok = false; + return {0, 0}; + } +} + +bool MessageListModel::matchMessage(const MessageId &id, const CanData &data, QMap &filters) { + auto cs = Qt::CaseInsensitive; + bool match = true; + bool convert_ok; + + for (int column = Column::NAME; column <= Column::DATA; column++) { + if (!filters.contains(column)) continue; + QString txt = filters[column]; + + QRegularExpression re(txt, QRegularExpression::CaseInsensitiveOption | QRegularExpression::DotMatchesEverythingOption); + + switch (column) { + case Column::NAME: + { + bool name_match = re.match(msgName(id)).hasMatch(); + + // Message signals + if (const auto msg = dbc()->msg(id)) { + for (auto s : msg->getSignals()) { + if (re.match(s->name).hasMatch()) { + name_match = true; + break; + } + } + } + if (!name_match) match = false; + } + break; + case Column::SOURCE: + { + auto source = parseRange(txt, &convert_ok); + bool source_match = convert_ok && (id.source >= source.first && id.source <= source.second); + if (!source_match) match = false; + } + break; + case Column::ADDRESS: + { + QString address_str = QString::number(id.address, 16); + bool address_re_match = re.match(address_str).capturedLength() == address_str.length(); + + auto address = parseRange(txt, &convert_ok, 16); + bool address_match = convert_ok && (id.address >= address.first && id.address <= address.second); + + if (!address_re_match && !address_match) match = false; + } + break; + case Column::FREQ: + { + // TODO: Hide stale messages? + auto freq = parseRange(txt, &convert_ok); + bool freq_match = convert_ok && (data.freq >= freq.first && data.freq <= freq.second); + if (!freq_match) match = false; + } + break; + case Column::COUNT: + { + auto count = parseRange(txt, &convert_ok); + bool count_match = convert_ok && (data.count >= count.first && data.count <= count.second); + if (!count_match) match = false; + } + break; + case Column::DATA: + { + bool data_match = false; + data_match |= QString(data.dat.toHex()).contains(txt, cs); + data_match |= re.match(QString(data.dat.toHex())).hasMatch(); + data_match |= re.match(QString(data.dat.toHex(' '))).hasMatch(); + + if (!data_match) match = false; + } + break; + } + } + return match; +} + + +void MessageListModel::fetchData() { + QList new_msgs; + for (auto it = can->last_msgs.begin(); it != can->last_msgs.end(); ++it) { + if (matchMessage(it.key(), it.value(), filter_str)) { + new_msgs.push_back(it.key()); + } } - if (msgs.size() != prev_row_count) { - sortMessages(); - return; + sortMessages(sort_order, sort_column, new_msgs); + + if (msgs != new_msgs) { + beginResetModel(); + msgs = new_msgs; + endResetModel(); } +} + +void MessageListModel::msgsReceived(const QHash *new_msgs) { + QList prev_msgs = msgs; + fetchData(); + for (int i = 0; i < msgs.size(); ++i) { if (new_msgs->contains(msgs[i])) { - for (int col = 3; col < columnCount(); ++col) + for (int col = Column::FREQ; col < columnCount(); ++col) emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole}); } } @@ -260,7 +372,7 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { if (column != columnCount() - 1) { sort_column = column; sort_order = order; - sortMessages(); + fetchData(); } } @@ -284,12 +396,17 @@ void MessageListModel::clearSuppress() { void MessageListModel::reset() { beginResetModel(); - filter_str = ""; + filter_str.clear(); msgs.clear(); clearSuppress(); endResetModel(); } +void MessageListModel::forceResetModel() { + beginResetModel(); + endResetModel(); +} + // MessageView void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -360,3 +477,74 @@ void MessageView::headerContextMenuEvent(const QPoint &pos) { menu->popup(header()->mapToGlobal(pos)); } + +MessageViewHeader::MessageViewHeader(QWidget *parent, MessageListModel *model) : model(model), QHeaderView(Qt::Horizontal, parent) { + QObject::connect(this, &QHeaderView::sectionResized, this, &MessageViewHeader::updateHeaderPositions); + QObject::connect(this, &QHeaderView::sectionMoved, this, &MessageViewHeader::updateHeaderPositions); +} + +void MessageViewHeader::showEvent(QShowEvent *e) { + + 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); + } + editors[i]->show(); + } + QHeaderView::showEvent(e); +} + +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() { + if (editors[0]) { + setViewportMargins(0, 0, 0, editors[0]->sizeHint().height()); + } else { + setViewportMargins(0, 0, 0, 0); + } + QHeaderView::updateGeometries(); + updateHeaderPositions(); +} + + +QSize MessageViewHeader::sizeHint() const { + QSize sz = QHeaderView::sizeHint(); + if (editors[0]) { + sz.setHeight(sz.height() + editors[0]->minimumSizeHint().height()); + } + return sz; +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index ef5467b053..c559467919 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -16,23 +17,37 @@ class MessageListModel : public QAbstractTableModel { Q_OBJECT public: + + enum Column { + NAME = 0, + SOURCE, + ADDRESS, + FREQ, + COUNT, + DATA, + }; + MessageListModel(QObject *parent) : QAbstractTableModel(parent) {} QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 6; } + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; int rowCount(const QModelIndex &parent = QModelIndex()) const override { return msgs.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; - void setFilterString(const QString &string); + void setFilterStrings(const QMap &filters); void msgsReceived(const QHash *new_msgs = nullptr); - void sortMessages(); + void fetchData(); void suppress(); void clearSuppress(); void reset(); + void forceResetModel(); QList msgs; QSet> suppressed_bytes; private: - QString filter_str; + static void sortMessages(Qt::SortOrder sort_order, int sort_column, QList &new_msgs); + static bool matchMessage(const MessageId &id, const CanData &data, QMap &filters); + + QMap filter_str; int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; }; @@ -48,6 +63,32 @@ public: void headerContextMenuEvent(const QPoint &pos); }; +class MessageViewHeader : public QHeaderView { + // https://stackoverflow.com/a/44346317 + + Q_OBJECT +public: + MessageViewHeader(QWidget *parent, MessageListModel *model); + void showEvent(QShowEvent *e) override; + void updateHeaderPositions(); + + void updateGeometries() override; + QSize sizeHint() const override; + +public slots: + void clearFilters(); + +signals: + void filtersUpdated(const QMap &filters); + +private: + void updateFilters(); + + QMap editors; + QMap> values; + MessageListModel *model; +}; + class MessagesWidget : public QWidget { Q_OBJECT @@ -59,15 +100,19 @@ public: void updateSuppressedButtons(); void reset(); +public slots: + void dbcModified(); + signals: void msgSelectionChanged(const MessageId &message_id); protected: MessageView *view; + MessageViewHeader *header; std::optional current_msg_id; - QLineEdit *filter; QCheckBox *multiple_lines_bytes; MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; + QLabel *num_msg_label; };