diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index a0cb09d07b..63e103793c 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -190,7 +190,7 @@ void ChartsWidget::updateState() { if (pos < 0 || pos > 0.8) { display_range.first = std::max(0.0, cur_sec - max_chart_range * 0.1); } - double max_sec = std::min(std::floor(display_range.first + max_chart_range), can->totalSeconds()); + double max_sec = std::min(display_range.first + max_chart_range, can->totalSeconds()); display_range.first = std::max(0.0, max_sec - max_chart_range); display_range.second = display_range.first + max_chart_range; } else if (cur_sec < (zoomed_range.first - 0.1) || cur_sec >= zoomed_range.second) { diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 87fa267d5a..472ffce104 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -2,6 +2,7 @@ #include #include +#include #include "tools/cabana/commands.h" #include "tools/cabana/mainwin.h" @@ -22,19 +23,15 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart // message title QHBoxLayout *title_layout = new QHBoxLayout(); title_layout->setContentsMargins(3, 6, 3, 0); - time_label = new QLabel(this); - time_label->setToolTip(tr("Current time")); - time_label->setStyleSheet("QLabel{font-weight:bold;}"); - title_layout->addWidget(time_label); - name_label = new ElidedLabel(this); + auto spacer = new QSpacerItem(0, 1); + title_layout->addItem(spacer); + title_layout->addWidget(name_label = new ElidedLabel(this), 1); name_label->setStyleSheet("QLabel{font-weight:bold;}"); name_label->setAlignment(Qt::AlignCenter); - name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - title_layout->addWidget(name_label); auto edit_btn = new ToolButton("pencil", tr("Edit Message")); title_layout->addWidget(edit_btn); - remove_btn = new ToolButton("x-lg", tr("Remove Message")); - title_layout->addWidget(remove_btn); + title_layout->addWidget(remove_btn = new ToolButton("x-lg", tr("Remove Message"))); + spacer->changeSize(edit_btn->sizeHint().width() * 2 + 9, 1); main_layout->addLayout(title_layout); // warning @@ -151,7 +148,6 @@ void DetailWidget::refresh() { } void DetailWidget::updateState(const QHash *msgs) { - time_label->setText(QString::number(can->currentSec(), 'f', 3)); if ((msgs && !msgs->contains(msg_id))) return; diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index ed6a865f53..5bb4b7f305 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -42,7 +42,7 @@ private: void updateState(const QHash * msgs = nullptr); MessageId msg_id; - QLabel *time_label, *warning_icon, *warning_label; + QLabel *warning_icon, *warning_label; ElidedLabel *name_label; QWidget *warning_widget; TabBar *tabbar; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index ee345c490c..c408179fdd 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -14,6 +14,7 @@ Settings settings; QSettings::Status Settings::save() { QSettings s(filePath(), QSettings::IniFormat); + s.setValue("absolute_time", absolute_time); s.setValue("fps", fps); s.setValue("max_cached_minutes", max_cached_minutes); s.setValue("chart_height", chart_height); @@ -40,6 +41,7 @@ QSettings::Status Settings::save() { void Settings::load() { QSettings s(filePath(), QSettings::IniFormat); + absolute_time = s.value("absolute_time", false).toBool(); fps = s.value("fps", 10).toInt(); max_cached_minutes = s.value("max_cached_minutes", 30).toInt(); chart_height = s.value("chart_height", 200).toInt(); diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index 42073a72de..da7781e6e4 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -29,6 +29,7 @@ public: void load(); inline static QString filePath() { return QApplication::applicationDirPath() + "/settings"; } + bool absolute_time = false; int fps = 10; int max_cached_minutes = 30; int chart_height = 200; diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 64c1991501..4a17affebe 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -8,6 +8,7 @@ #include #include +#include #include #include "common/timing.h" @@ -64,6 +65,7 @@ public: virtual void seekTo(double ts) {} virtual QString routeName() const = 0; virtual QString carFingerprint() const { return ""; } + virtual QDateTime beginDateTime() const { return {}; } virtual double routeStartTime() const { return 0; } virtual double currentSec() const = 0; virtual double totalSeconds() const { return lastEventMonoTime() / 1e9 - routeStartTime(); } diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 17805a0b66..fd140eb8bf 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -46,6 +46,7 @@ void LiveStream::start() { emit streamStarted(); stream_thread->start(); startUpdateTimer(); + begin_date_time = QDateTime::currentDateTime(); } LiveStream::~LiveStream() { diff --git a/tools/cabana/streams/livestream.h b/tools/cabana/streams/livestream.h index 38ef2c67f9..d72112c604 100644 --- a/tools/cabana/streams/livestream.h +++ b/tools/cabana/streams/livestream.h @@ -15,6 +15,7 @@ public: LiveStream(QObject *parent); virtual ~LiveStream(); void start() override; + inline QDateTime beginDateTime() const { return begin_date_time; } inline double routeStartTime() const override { return begin_event_ts / 1e9; } inline double currentSec() const override { return (current_event_ts - begin_event_ts) / 1e9; } void setSpeed(float speed) override { speed_ = speed; } @@ -49,6 +50,7 @@ private: int timer_id; QBasicTimer update_timer; + QDateTime begin_date_time; uint64_t begin_event_ts = 0; uint64_t current_event_ts = 0; uint64_t first_event_ts = 0; diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index 3f301d2ce2..d69922a432 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -22,11 +22,13 @@ public: inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } double totalSeconds() const override { return replay->totalSeconds(); } inline VisionStreamType visionStreamType() const override { return replay->hasFlag(REPLAY_FLAG_ECAM) ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD; } + inline QDateTime beginDateTime() const { return replay->route()->datetime(); } inline double routeStartTime() const override { return replay->routeStartTime() / (double)1e9; } inline double currentSec() const override { return replay->currentSeconds(); } inline const Route *route() const override { return replay->route(); } inline void setSpeed(float speed) override { replay->setSpeed(speed); } inline float getSpeed() const { return replay->getSpeed(); } + inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; inline const std::vector> getTimeline() override { return replay->getTimeline(); } diff --git a/tools/cabana/util.cc b/tools/cabana/util.cc index 41d55b793f..38190da301 100644 --- a/tools/cabana/util.cc +++ b/tools/cabana/util.cc @@ -242,6 +242,13 @@ void setTheme(int theme) { } } +QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { + QString format = absolute_time ? "yyyy-MM-dd hh:mm:ss" + : (sec > 60 * 60 ? "hh:mm:ss" : "mm:ss"); + if (include_milliseconds) format += ".zzz"; + return QDateTime::fromMSecsSinceEpoch(sec * 1000).toString(format); +} + } // namespace utils QString toHex(uint8_t byte) { diff --git a/tools/cabana/util.h b/tools/cabana/util.h index 0409d5c825..da476ab31a 100644 --- a/tools/cabana/util.h +++ b/tools/cabana/util.h @@ -103,9 +103,7 @@ public: namespace utils { QPixmap icon(const QString &id); void setTheme(int theme); -inline QString formatSeconds(int seconds) { - return QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC).toString(seconds > 60 * 60 ? "hh:mm:ss" : "mm:ss"); -} +QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { auto size = (r.size() - text.size()) / 2; p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 3e22464d06..0a1d2855a5 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -3,8 +3,9 @@ #include #include -#include -#include +#include +#include +#include #include #include #include @@ -27,50 +28,18 @@ static const QColor timeline_colors[] = { VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { setFrameStyle(QFrame::StyledPanel | QFrame::Plain); auto main_layout = new QVBoxLayout(this); - if (!can->liveStreaming()) { + if (!can->liveStreaming()) main_layout->addWidget(createCameraWidget()); - } - - // btn controls - QButtonGroup *group = new QButtonGroup(this); - group->setExclusive(true); + main_layout->addLayout(createPlaybackController()); - QHBoxLayout *control_layout = new QHBoxLayout(); - play_btn = new QToolButton(); - play_btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - control_layout->addWidget(play_btn); - if (can->liveStreaming()) { - control_layout->addWidget(skip_to_end_btn = new QToolButton(this)); - skip_to_end_btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - skip_to_end_btn->setIcon(utils::icon("skip-end-fill")); - skip_to_end_btn->setToolTip(tr("Skip to the end")); - QObject::connect(skip_to_end_btn, &QToolButton::clicked, [group]() { - // set speed to 1.0 - group->buttons()[2]->click(); - can->pause(false); - can->seekTo(can->totalSeconds() + 1); - }); - } - - for (float speed : {0.1, 0.5, 1., 2.}) { - QToolButton *btn = new QToolButton(this); - btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); - btn->setText(QString("%1x").arg(speed)); - btn->setCheckable(true); - QObject::connect(btn, &QToolButton::clicked, [speed]() { can->setSpeed(speed); }); - control_layout->addWidget(btn); - group->addButton(btn); - if (speed == 1.0) btn->setChecked(true); - } - main_layout->addLayout(control_layout); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); + QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); QObject::connect(&settings, &Settings::changed, this, &VideoWidget::updatePlayBtnState); - updatePlayBtnState(); + updatePlayBtnState(); setWhatsThis(tr(R"( Video
@@ -93,6 +62,71 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { timeline_colors[(int)TimelineType::AlertCritical].name())); } +QHBoxLayout *VideoWidget::createPlaybackController() { + QHBoxLayout *layout = new QHBoxLayout(); + layout->addWidget(seek_backward_btn = new ToolButton("rewind", tr("Seek backward"))); + layout->addWidget(play_btn = new ToolButton("play", tr("Play"))); + layout->addWidget(seek_forward_btn = new ToolButton("fast-forward", tr("Seek forward"))); + + if (can->liveStreaming()) { + layout->addWidget(skip_to_end_btn = new ToolButton("skip-end", tr("Skip to the end"), this)); + QObject::connect(skip_to_end_btn, &QToolButton::clicked, [this]() { + // set speed to 1.0 + speed_btn->menu()->actions()[7]->setChecked(true); + can->pause(false); + can->seekTo(can->totalSeconds() + 1); + }); + } + + layout->addWidget(time_btn = new QToolButton); + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + time_btn->setAutoRaise(true); + layout->addStretch(0); + + if (!can->liveStreaming()) { + layout->addWidget(loop_btn = new ToolButton("repeat", tr("Loop playback"))); + QObject::connect(loop_btn, &QToolButton::clicked, this, &VideoWidget::loopPlaybackClicked); + } + + // speed selector + layout->addWidget(speed_btn = new QToolButton(this)); + speed_btn->setAutoRaise(true); + speed_btn->setMenu(new QMenu(speed_btn)); + speed_btn->setPopupMode(QToolButton::InstantPopup); + QActionGroup *speed_group = new QActionGroup(this); + speed_group->setExclusive(true); + + int max_width = 0; + QFont font = speed_btn->font(); + font.setBold(true); + speed_btn->setFont(font); + QFontMetrics fm(font); + for (float speed : {0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 0.8, 1., 2., 3., 5.}) { + QString name = QString("%1x").arg(speed); + max_width = std::max(max_width, fm.width(name) + fm.horizontalAdvance(QLatin1Char(' ')) * 2); + + QAction *act = new QAction(name, speed_group); + act->setCheckable(true); + QObject::connect(act, &QAction::toggled, [this, speed]() { + can->setSpeed(speed); + speed_btn->setText(QString("%1x ").arg(speed)); + }); + speed_btn->menu()->addAction(act); + if (speed == 1.0)act->setChecked(true); + } + speed_btn->setMinimumWidth(max_width + style()->pixelMetric(QStyle::PM_MenuButtonIndicator)); + + QObject::connect(play_btn, &QToolButton::clicked, []() { can->pause(!can->isPaused()); }); + QObject::connect(seek_backward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() - 1); }); + QObject::connect(seek_forward_btn, &QToolButton::clicked, []() { can->seekTo(can->currentSec() + 1); }); + QObject::connect(time_btn, &QToolButton::clicked, [this]() { + settings.absolute_time = !settings.absolute_time; + time_btn->setToolTip(settings.absolute_time ? tr("Elapsed time") : tr("Absolute time")); + updateState(); + }); + return layout; +} + QWidget *VideoWidget::createCameraWidget() { QWidget *w = new QWidget(this); QVBoxLayout *l = new QVBoxLayout(w); @@ -106,30 +140,30 @@ QWidget *VideoWidget::createCameraWidget() { stacked->addWidget(alert_label = new InfoLabel(this)); l->addLayout(stacked); - // slider controls - auto slider_layout = new QHBoxLayout(); - slider_layout->addWidget(time_label = new QLabel("00:00")); - - slider = new Slider(this); + l->addWidget(slider = new Slider(this)); slider->setSingleStep(0); - slider_layout->addWidget(slider); - - slider_layout->addWidget(end_time_label = new QLabel(this)); - l->addLayout(slider_layout); setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(slider, &QSlider::valueChanged, [=](int value) { time_label->setText(utils::formatSeconds(slider->currentSecond())); }); QObject::connect(slider, &Slider::updateMaximumTime, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog); - QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); return w; } +void VideoWidget::loopPlaybackClicked() { + auto replay = qobject_cast(can)->getReplay(); + if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { + replay->removeFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat"); + } else { + replay->addFlag(REPLAY_FLAG_NO_LOOP); + loop_btn->setIcon("repeat-1"); + } +} + void VideoWidget::setMaximumTime(double sec) { maximum_time = sec; - end_time_label->setText(utils::formatSeconds(sec)); slider->setTimeRange(0, sec); } @@ -143,19 +177,29 @@ void VideoWidget::updateTimeRange(double min, double max, bool is_zoomed) { min = 0; max = maximum_time; } - end_time_label->setText(utils::formatSeconds(max)); slider->setTimeRange(min, max); } +QString VideoWidget::formatTime(double sec, bool include_milliseconds) { + if (settings.absolute_time) + sec = can->beginDateTime().addMSecs(sec * 1000).toMSecsSinceEpoch() / 1000.0; + return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); +} + void VideoWidget::updateState() { - if (!slider->isSliderDown()) { - slider->setCurrentSecond(can->currentSec()); + if (slider) { + if (!slider->isSliderDown()) + slider->setCurrentSecond(can->currentSec()); + alert_label->showAlert(slider->alertInfo(can->currentSec())); + time_btn->setText(QString("%1 / %2").arg(formatTime(can->currentSec(), true), + formatTime(slider->maximum() / slider->factor))); + } else { + time_btn->setText(formatTime(can->currentSec(), true)); } - alert_label->showAlert(slider->alertInfo(can->currentSec())); } void VideoWidget::updatePlayBtnState() { - play_btn->setIcon(utils::icon(can->isPaused() ? "play" : "pause")); + play_btn->setIcon(can->isPaused() ? "play" : "pause"); play_btn->setToolTip(can->isPaused() ? tr("Play") : tr("Pause")); } @@ -284,8 +328,7 @@ void InfoLabel::showPixmap(const QPoint &pt, const QString &sec, const QPixmap & second = sec; pixmap = pm; alert_info = alert; - resize(pm.size()); - move(pt); + setGeometry(QRect(pt, pm.size())); setVisible(true); update(); } diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 12961cd061..68e0461117 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include @@ -39,6 +39,8 @@ public: QPixmap thumbnail(double sec); void parseQLog(int segnum, std::shared_ptr qlog); + const double factor = 1000.0; + signals: void updateMaximumTime(double); @@ -48,7 +50,6 @@ private: bool event(QEvent *event) override; void paintEvent(QPaintEvent *ev) override; - const double factor = 1000.0; QMap thumbnails; std::map alerts; InfoLabel thumbnail_label; @@ -63,16 +64,22 @@ public: void setMaximumTime(double sec); protected: + QString formatTime(double sec, bool include_milliseconds = false); void updateState(); void updatePlayBtnState(); QWidget *createCameraWidget(); + QHBoxLayout *createPlaybackController(); + void loopPlaybackClicked(); CameraWidget *cam_widget; double maximum_time = 0; - QLabel *end_time_label; - QLabel *time_label; - QToolButton *play_btn; - QToolButton *skip_to_end_btn = nullptr; - InfoLabel *alert_label; - Slider *slider; + QToolButton *time_btn = nullptr; + ToolButton *seek_backward_btn = nullptr; + ToolButton *play_btn = nullptr; + ToolButton *seek_forward_btn = nullptr; + ToolButton *loop_btn = nullptr; + QToolButton *speed_btn = nullptr; + ToolButton *skip_to_end_btn = nullptr; + InfoLabel *alert_label = nullptr; + Slider *slider = nullptr; };