diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index bcb65e2e3e..a4b7764346 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -179,6 +179,7 @@ void MainWindow::createDockWindows() { void MainWindow::createDockWidgets() { messages_widget = new MessagesWidget(this); messages_dock->setWidget(messages_widget); + QObject::connect(messages_widget, &MessagesWidget::titleChanged, messages_dock, &QDockWidget::setWindowTitle); // right panel charts_widget = new ChartsWidget(this); diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 720553dcb3..8043b99a70 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -6,16 +6,29 @@ #include #include #include +#include #include #include #include #include "tools/cabana/commands.h" +static bool isMessageActive(const MessageId &id) { + if (auto dummy_stream = dynamic_cast(can)) { + return true; + } + if (id.source == INVALID_SOURCE) { + return false; + } + // Check if the message is active based on time difference and frequency + const auto &m = can->lastMessage(id); + float delta = can->currentSec() - m.ts; + return (m.freq == 0 && delta < 1.5) || (m.freq > 0 && ((delta - 1.0 / settings.fps) < (5.0 / m.freq))); +} + 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 @@ -39,23 +52,10 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget header->setStretchLastSection(true); header->setContextMenuPolicy(Qt::CustomContextMenu); - // suppress - QHBoxLayout *suppress_layout = new QHBoxLayout(); - suppress_layout->addWidget(suppress_add = new QPushButton("Suppress Highlighted")); - suppress_layout->addWidget(suppress_clear = new QPushButton()); - suppress_clear->setToolTip(tr("Clear suppressed")); - suppress_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); - 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::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); - QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals); 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); @@ -75,9 +75,6 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget } } }); - QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - suppressHighlighted(); setWhatsThis(tr(R"( Message View
@@ -91,18 +88,30 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget )")); } -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); +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 views_btn = toolbar->addAction(utils::icon("three-dots"), tr("View...")); - views_btn->setMenu(menu); - auto view_button = qobject_cast(toolbar->widgetForAction(views_btn)); + auto view_button = new ToolButton("three-dots", tr("View...")); + view_button->setMenu(menu); view_button->setPopupMode(QToolButton::InstantPopup); - view_button->setToolButtonStyle(Qt::ToolButtonIconOnly); 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; } @@ -113,7 +122,7 @@ void MessagesWidget::updateTitle() { auto m = dbc()->msg(item.id); return m ? std::make_pair(pair.first + 1, pair.second + m->sigs.size()) : pair; }); - num_msg_label->setText(tr("%1 Messages (%2 DBC Messages, %3 Signals)") + emit titleChanged(tr("%1 Messages (%2 DBC Messages, %3 Signals)") .arg(model->items_.size()).arg(stats.first).arg(stats.second)); } @@ -156,6 +165,10 @@ void MessagesWidget::menuAboutToShow() { 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) { @@ -186,9 +199,9 @@ QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, QVariant MessageListModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= items_.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); + auto getFreq = [](float freq) { + if (freq > 0) { + return freq >= 0.95 ? QString::number(std::nearbyint(freq)) : QString::number(freq, 'f', 2); } else { return QStringLiteral("--"); } @@ -202,7 +215,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : "N/A"; case Column::ADDRESS: return QString::number(item.id.address, 16); case Column::NODE: return item.node; - case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(data) : "N/A"; + case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(data.freq) : "N/A"; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(data.count) : "N/A"; case Column::DATA: return item.id.source != INVALID_SOURCE ? "" : "N/A"; } @@ -210,6 +223,8 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { return QVariant::fromValue((void*)(&data.colors)); } else if (role == BytesRole && index.column() == Column::DATA && item.id.source != INVALID_SOURCE) { return QVariant::fromValue((void*)(&data.dat)); + } else if (role == Qt::ForegroundRole && !item.active) { + return settings.theme == DARK_THEME ? QApplication::palette().color(QPalette::Text).darker(150) : QColor(Qt::gray); } else if (role == Qt::ToolTipRole && index.column() == Column::NAME) { auto msg = dbc()->msg(item.id); auto tooltip = item.name; @@ -224,6 +239,11 @@ void MessageListModel::setFilterStrings(const QMap &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)) { @@ -233,19 +253,22 @@ void MessageListModel::dbcModified() { } void MessageListModel::sortItems(std::vector &items) { - auto do_sort = [order = sort_order](std::vector &m, auto proj) { - std::stable_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); - }); + auto compare = [this](const auto &l, const auto &r) { + switch (sort_column) { + case Column::NAME: return l.name < r.name; + case Column::SOURCE: return l.id.source < r.id.source; + case Column::ADDRESS: return l.id.address < r.id.address; + case Column::NODE: return l.node < r.node; + case Column::FREQ: return can->lastMessage(l.id).freq < can->lastMessage(r.id).freq; + case Column::COUNT: return can->lastMessage(l.id).count < can->lastMessage(r.id).count; + default: return false; // Default case to suppress compiler warning + } }; - switch (sort_column) { - case Column::NAME: do_sort(items, [](auto &item) { return std::tie(item.name, item.id); }); break; - case Column::SOURCE: do_sort(items, [](auto &item) { return std::tie(item.id.source, item.id); }); break; - case Column::ADDRESS: do_sort(items, [](auto &item) { return std::tie(item.id.address, item.id);}); break; - case Column::NODE: do_sort(items, [](auto &item) { return std::tie(item.node, item.id);}); break; - case Column::FREQ: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).freq, item.id); }); break; - case Column::COUNT: do_sort(items, [](auto &item) { return std::make_pair(can->lastMessage(item.id).count, item.id); }); break; - } + + 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) { @@ -292,7 +315,6 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = item.node.contains(txt, Qt::CaseInsensitive); break; case Column::FREQ: - // TODO: Hide stale messages? match = parseRange(txt, data.freq); break; case Column::COUNT: @@ -306,7 +328,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { return match; } -void MessageListModel::filterAndSort() { +bool MessageListModel::filterAndSort() { // merge CAN and DBC messages std::vector all_messages; all_messages.reserve(can->lastMessages().size() + dbc_messages_.size()); @@ -319,13 +341,18 @@ void MessageListModel::filterAndSort() { // filter and sort std::vector items; + items.reserve(all_messages.size()); for (const auto &id : all_messages) { - 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); + bool active = isMessageActive(id); + if (active || show_inactive_messages) { + auto msg = dbc()->msg(id); + Item item = {.id = id, + .active = active, + .name = msg ? msg->name : UNTITLED, + .node = msg ? msg->transmitter : QString()}; + if (match(item)) + items.emplace_back(item); + } } sortItems(items); @@ -333,16 +360,21 @@ void MessageListModel::filterAndSort() { 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_.contains(Column::FREQ) || filters_.contains(Column::COUNT) || filters_.contains(Column::DATA)) { - filterAndSort(); + if (filterAndSort()) return; } for (int i = 0; i < items_.size(); ++i) { - if (!new_msgs || new_msgs->count(items_[i].id)) { - for (int col = Column::FREQ; col < columnCount(); ++col) + auto &item = items_[i]; + bool prev_active = item.active; + item.active = isMessageActive(item.id); + if (item.active != prev_active || !new_msgs || new_msgs->count(item.id)) { + for (int col = 0; col < columnCount(); ++col) emit dataChanged(index(i, col), index(i, col), {Qt::DisplayRole}); } } @@ -360,20 +392,18 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { 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); + 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) { - painter->translate(header()->sectionSize(header()->logicalIndex(i)), 0); - painter->drawLine(0, y, 0, y + option.rect.height()); + int sectionX = header()->sectionViewportPosition(i); + painter->drawLine(sectionX, option.rect.top(), sectionX, option.rect.bottom()); } - painter->setPen(old_pen); - painter->resetTransform(); + painter->setPen(oldPen); } void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index e7f1f8c033..c110db2b56 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -7,10 +7,8 @@ #include #include -#include #include #include -#include #include #include @@ -38,19 +36,22 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); } void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; void setFilterStrings(const QMap &filters); + void showInactivemessages(bool show); void msgsReceived(const std::set *new_msgs, bool has_new_ids); - void filterAndSort(); + bool filterAndSort(); void dbcModified(); struct Item { MessageId id; QString name; QString node; + bool active; bool operator==(const Item &other) const { return id == other.id && name == other.name && node == other.node; } }; std::vector items_; + bool show_inactive_messages = true; private: void sortItems(std::vector &items); @@ -100,9 +101,10 @@ public: signals: void msgSelectionChanged(const MessageId &message_id); + void titleChanged(const QString &title); protected: - QToolBar *createToolBar(); + QWidget *createToolBar(); void headerContextMenuEvent(const QPoint &pos); void menuAboutToShow(); void setMultiLineBytes(bool multi); @@ -115,6 +117,5 @@ protected: MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; - QLabel *num_msg_label; QMenu *menu; }; diff --git a/tools/cabana/utils/util.cc b/tools/cabana/utils/util.cc index c89e39993b..a5f6cf0f5e 100644 --- a/tools/cabana/utils/util.cc +++ b/tools/cabana/utils/util.cc @@ -88,18 +88,24 @@ void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem & const auto &bytes = *static_cast*>(data.value()); const auto &colors = *static_cast*>(index.data(ColorsRole).value()); + auto text_color = index.data(Qt::ForegroundRole).value(); + bool inactive = text_color.isValid(); + if (!inactive) { + text_color = option.palette.color(QPalette::Text); + } + for (int i = 0; i < bytes.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) { + if (!inactive && i < colors.size() && colors[i].alpha() > 0) { if (option.state & QStyle::State_Selected) { painter->setPen(option.palette.color(QPalette::Text)); painter->fillRect(r, option.palette.color(QPalette::Window)); } painter->fillRect(r, colors[i]); - } else if (option.state & QStyle::State_Selected) { - painter->setPen(option.palette.color(QPalette::HighlightedText)); + } else { + painter->setPen(option.state & QStyle::State_Selected ? option.palette.color(QPalette::HighlightedText) : text_color); } utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); } diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index 3d5d3219c6..ae148f1a5b 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -96,20 +96,26 @@ void Replay::updateEvents(const std::function &lambda) { } void Replay::seekTo(double seconds, bool relative) { - seconds = relative ? seconds + currentSeconds() : seconds; + seeking_to_seconds_ = relative ? seconds + currentSeconds() : seconds; + seeking_to_seconds_ = std::max(double(0.0), seeking_to_seconds_); + updateEvents([&]() { - seconds = std::max(double(0.0), seconds); - int seg = (int)seconds / 60; - if (segments_.find(seg) == segments_.end()) { - rWarning("can't seek to %d s segment %d is invalid", seconds, seg); + int target_segment = (int)seeking_to_seconds_ / 60; + if (segments_.count(target_segment) == 0) { + rWarning("can't seek to %d s segment %d is invalid", (int)seeking_to_seconds_, target_segment); return true; } - rInfo("seeking to %d s, segment %d", (int)seconds, seg); - current_segment_ = seg; - cur_mono_time_ = route_start_ts_ + seconds * 1e9; - emit seekedTo(seconds); - return isSegmentMerged(seg); + rInfo("seeking to %d s, segment %d", (int)seeking_to_seconds_, target_segment); + current_segment_ = target_segment; + cur_mono_time_ = route_start_ts_ + seeking_to_seconds_ * 1e9; + bool segment_merged = isSegmentMerged(target_segment); + if (segment_merged) { + emit seekedTo(seeking_to_seconds_); + // Reset seeking_to_seconds_ to indicate completion of seek + seeking_to_seconds_ = -1; + } + return segment_merged; }); queueSegment(); } @@ -277,6 +283,18 @@ void Replay::mergeSegments(const SegmentMap::iterator &begin, const SegmentMap:: if (stream_thread_) { emit segmentsMerged(); + + // Check if seeking is in progress + if (seeking_to_seconds_ >= 0) { + int target_segment = int(seeking_to_seconds_ / 60); + auto segment_found = std::find(segments_need_merge.begin(), segments_need_merge.end(), target_segment); + + // If the target segment is found, emit seekedTo signal and reset seeking_to_seconds_ + if (segment_found != segments_need_merge.end()) { + emit seekedTo(seeking_to_seconds_); + seeking_to_seconds_ = -1; // Reset seeking_to_seconds_ to indicate completion of seek + } + } } updateEvents([&]() { events_.swap(new_events_); diff --git a/tools/replay/replay.h b/tools/replay/replay.h index 3859b69380..c4140dc806 100644 --- a/tools/replay/replay.h +++ b/tools/replay/replay.h @@ -117,6 +117,7 @@ protected: std::condition_variable stream_cv_; std::atomic updating_events_ = false; std::atomic current_segment_ = 0; + double seeking_to_seconds_ = -1; SegmentMap segments_; // the following variables must be protected with stream_lock_ std::atomic exit_ = false;