cabana: add filter per column (#28034)

* add non functional lineedits

* implement actual filtering

* more efficient modelReset

* less diff

* refactor contains

* make sortMessages static

* cleanup

* regex support

* case insensitive

* uncomment

* set header before restoring state

* force reset

* more regex matching

* update comment

* put back header with clear button

* handle hiding

* handle horizontal scrolling

* whitespace

* dont add that
old-commit-hash: aa54204c81
beeps
Willem Melching 2 years ago committed by GitHub
parent 10215bc44e
commit 3cd1f3560b
  1. 324
      tools/cabana/messageswidget.cc
  2. 55
      tools/cabana/messageswidget.h

@ -1,42 +1,46 @@
#include "tools/cabana/messageswidget.h"
#include <QHBoxLayout>
#include <QPainter>
#include <QPushButton>
#include <QScrollBar>
#include <QVBoxLayout>
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"(
<b>Message View</b><br/>
@ -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<QColor> 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<int, QString> &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<MessageId> &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<MessageId, CanData> *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<unsigned int, unsigned int> 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<unsigned int>::min();
unsigned int parsed2 = std::numeric_limits<unsigned int>::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<int, QString> &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<MessageId> 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<MessageId, CanData> *new_msgs) {
QList<MessageId> 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<int, QString> 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;
}

@ -4,6 +4,7 @@
#include <QCheckBox>
#include <QContextMenuEvent>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QSet>
@ -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<int, QString> &filters);
void msgsReceived(const QHash<MessageId, CanData> *new_msgs = nullptr);
void sortMessages();
void fetchData();
void suppress();
void clearSuppress();
void reset();
void forceResetModel();
QList<MessageId> msgs;
QSet<std::pair<MessageId, int>> suppressed_bytes;
private:
QString filter_str;
static void sortMessages(Qt::SortOrder sort_order, int sort_column, QList<MessageId> &new_msgs);
static bool matchMessage(const MessageId &id, const CanData &data, QMap<int, QString> &filters);
QMap<int, QString> 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<int, QString> &filters);
private:
void updateFilters();
QMap<int, QLineEdit *> editors;
QMap<int, QSet<QString>> 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<MessageId> current_msg_id;
QLineEdit *filter;
QCheckBox *multiple_lines_bytes;
MessageListModel *model;
QPushButton *suppress_add;
QPushButton *suppress_clear;
QLabel *num_msg_label;
};

Loading…
Cancel
Save