diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 8dd0bac820..abc8c755ff 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -1,6 +1,7 @@ #include "tools/cabana/messageswidget.h" #include +#include #include #include @@ -9,29 +10,30 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { main_layout->setContentsMargins(0 ,0, 0, 0); // message filter - filter = new QLineEdit(this); + 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")); - main_layout->addWidget(filter); + 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); + main_layout->addLayout(title_layout); // message table - table_widget = new QTableView(this); + view = new MessageView(this); model = new MessageListModel(this); - table_widget->setModel(model); - table_widget->setItemDelegateForColumn(5, new MessageBytesDelegate(table_widget)); - table_widget->setSelectionBehavior(QAbstractItemView::SelectRows); - table_widget->setSelectionMode(QAbstractItemView::SingleSelection); - table_widget->setSortingEnabled(true); - table_widget->sortByColumn(0, Qt::AscendingOrder); - table_widget->setColumnWidth(0, 150); - table_widget->setColumnWidth(1, 50); - table_widget->setColumnWidth(2, 50); - table_widget->setColumnWidth(3, 50); - table_widget->horizontalHeader()->setStretchLastSection(true); - table_widget->verticalHeader()->hide(); - main_layout->addWidget(table_widget); + auto delegate = new MessageBytesDelegate(view, settings.multiple_lines_bytes); + view->setItemDelegateForColumn(5, delegate); + view->setModel(model); + view->setSortingEnabled(true); + view->sortByColumn(0, Qt::AscendingOrder); + view->setItemsExpandable(false); + view->setIndentation(0); + view->setRootIsDecorated(false); + view->header()->setStretchLastSection(true); + main_layout->addWidget(view); // suppress QHBoxLayout *suppress_layout = new QHBoxLayout(); @@ -43,6 +45,11 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { // signals/slots QObject::connect(filter, &QLineEdit::textEdited, model, &MessageListModel::setFilterString); + QObject::connect(multiple_lines_bytes, &QCheckBox::stateChanged, [=](int state) { + settings.multiple_lines_bytes = (state == Qt::Checked); + delegate->setMultipleLines(settings.multiple_lines_bytes); + model->sortMessages(); + }); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); QObject::connect(can, &AbstractStream::streamStarted, this, &MessagesWidget::reset); QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::sortMessages); @@ -53,7 +60,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { selectMessage(*current_msg_id); } }); - QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { + 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) { @@ -85,7 +92,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { void MessagesWidget::selectMessage(const MessageId &msg_id) { if (int row = model->msgs.indexOf(msg_id); row != -1) { - table_widget->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); + view->selectionModel()->setCurrentIndex(model->index(row, 0), QItemSelectionModel::Rows | QItemSelectionModel::ClearAndSelect); } } @@ -101,7 +108,7 @@ void MessagesWidget::updateSuppressedButtons() { void MessagesWidget::reset() { current_msg_id = std::nullopt; - table_widget->selectionModel()->clear(); + view->selectionModel()->clear(); model->reset(); filter->clear(); updateSuppressedButtons(); @@ -111,8 +118,10 @@ void MessagesWidget::reset() { // MessageListModel QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { - if (orientation == Qt::Horizontal && role == Qt::DisplayRole) - return (QString[]){"Name", "Bus", "ID", "Freq", "Count", "Bytes"}[section]; + if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { + static const QString titles[] = {"Name", "Bus", "ID", "Freq", "Count", "Bytes"}; + return titles[section]; + } return {}; } @@ -254,3 +263,22 @@ void MessageListModel::reset() { clearSuppress(); endResetModel(); } + +// MessageView + +void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + QTreeView::drawRow(painter, option, index); + painter->save(); + const int gridHint = style()->styleHint(QStyle::SH_Table_GridLineColor, &option, this); + const QColor gridColor = QColor::fromRgba(static_cast(gridHint)); + 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(i), 0); + painter->drawLine(0, y, 0, y + option.rect.height()); + } + painter->restore(); +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index d4ef896519..cc48b70bf4 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -1,10 +1,11 @@ #pragma once #include +#include #include #include #include -#include +#include #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" @@ -34,14 +35,21 @@ private: Qt::SortOrder sort_order = Qt::AscendingOrder; }; +class MessageView : public QTreeView { + Q_OBJECT +public: + MessageView(QWidget *parent) : QTreeView(parent) {} + void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; +}; + class MessagesWidget : public QWidget { Q_OBJECT public: MessagesWidget(QWidget *parent); void selectMessage(const MessageId &message_id); - QByteArray saveHeaderState() const { return table_widget->horizontalHeader()->saveState(); } - bool restoreHeaderState(const QByteArray &state) const { return table_widget->horizontalHeader()->restoreState(state); } + QByteArray saveHeaderState() const { return view->header()->saveState(); } + bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); } void updateSuppressedButtons(); void reset(); @@ -49,11 +57,11 @@ signals: void msgSelectionChanged(const MessageId &message_id); protected: - QTableView *table_widget; + MessageView *view; std::optional current_msg_id; QLineEdit *filter; + QCheckBox *multiple_lines_bytes; MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; - }; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 3a1fc14751..8fd852e198 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -31,6 +31,7 @@ void Settings::save() { s.setValue("chart_series_type", chart_series_type); s.setValue("theme", theme); s.setValue("sparkline_range", sparkline_range); + s.setValue("multiple_lines_bytes", multiple_lines_bytes); } void Settings::load() { @@ -50,6 +51,7 @@ void Settings::load() { chart_series_type = s.value("chart_series_type", 0).toInt(); theme = s.value("theme", 0).toInt(); sparkline_range = s.value("sparkline_range", 15).toInt(); + multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool(); } // SettingsDlg diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index dd01b602e7..04070b0909 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -24,6 +24,7 @@ public: int chart_series_type = 0; int theme = 0; int sparkline_range = 15; // 15 seconds + bool multiple_lines_bytes = true; QString last_dir; QString last_route_dir; QByteArray geometry; diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index 58c1deb349..584938d0d9 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -41,9 +41,35 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in // MessageBytesDelegate -MessageBytesDelegate::MessageBytesDelegate(QObject *parent) : QStyledItemDelegate(parent) { +MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) : multiple_lines(multiple_lines), QStyledItemDelegate(parent) { fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_width = QFontMetrics(fixed_font).width("00 "); + byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); +} + +void MessageBytesDelegate::setMultipleLines(bool v) { + if (std::exchange(multiple_lines, v) != multiple_lines) { + std::fill_n(size_cache, std::size(size_cache), QSize{}); + } +} + +QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + int n = index.data(BytesRole).toByteArray().size(); + if (n <= 0 || n > 64) return {}; + + QSize size = size_cache[n - 1]; + if (size.isEmpty()) { + int h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + int v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; + if (!multiple_lines) { + size.setWidth(h_margin * 2 + n * byte_size.width()); + size.setHeight(byte_size.height() + 2 * v_margin); + } else { + size.setWidth(h_margin * 2 + 8 * byte_size.width()); + size.setHeight(byte_size.height() * std::max(1, n / 8) + 2 * v_margin); + } + size_cache[n - 1] = size; + } + return size; } void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { @@ -52,18 +78,24 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & int v_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameVMargin); int h_margin = option.widget->style()->pixelMetric(QStyle::PM_FocusFrameHMargin); - QRect rc{option.rect.left() + h_margin, option.rect.top() + v_margin, byte_width, option.rect.height() - 2 * v_margin}; + painter->save(); + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + painter->setPen(option.palette.color(QPalette::HighlightedText)); + } - auto color_role = option.state & QStyle::State_Selected ? QPalette::HighlightedText : QPalette::Text; - painter->setPen(option.palette.color(color_role)); + const QPoint pt{option.rect.left() + h_margin, option.rect.top() + v_margin}; painter->setFont(fixed_font); for (int i = 0; i < byte_list.size(); ++i) { + int row = !multiple_lines ? 0 : i / 8; + int column = !multiple_lines ? i : i % 8; + QRect r = QRect({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); if (i < colors.size() && colors[i].alpha() > 0) { - painter->fillRect(rc, colors[i]); + painter->fillRect(r, colors[i]); } - painter->drawText(rc, Qt::AlignCenter, toHex(byte_list[i])); - rc.moveLeft(rc.right() + 1); + painter->drawText(r, Qt::AlignCenter, toHex(byte_list[i])); } + painter->restore(); } QColor getColor(const cabana::Signal *sig) { diff --git a/tools/cabana/util.h b/tools/cabana/util.h index 3b3a10ea17..f9eaf7bc2b 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -63,10 +63,16 @@ private: class MessageBytesDelegate : public QStyledItemDelegate { Q_OBJECT public: - MessageBytesDelegate(QObject *parent); + MessageBytesDelegate(QObject *parent, bool multiple_lines = false); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setMultipleLines(bool v); + +private: QFont fixed_font; - int byte_width; + QSize byte_size = {}; + bool multiple_lines = false; + mutable QSize size_cache[64] = {}; }; inline QString toHex(const QByteArray &dat) { return dat.toHex(' ').toUpper(); }