From a18df70a3fa8c65e9bf5db0831b01c33bfe5da0a Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Thu, 11 Jul 2024 06:10:24 +0800 Subject: [PATCH] cabana: simplify stream management and remove problematic autosave feature (#32945) simplify code and remove problematic code old-commit-hash: c17c34187b6e9c14b3c00135386625bbb601ed22 --- tools/cabana/cabana.cc | 29 +--- tools/cabana/chart/chart.cc | 9 +- tools/cabana/chart/chartswidget.h | 1 - tools/cabana/chart/signalselector.cc | 3 +- tools/cabana/chart/sparkline.cc | 9 +- tools/cabana/chart/sparkline.h | 1 + tools/cabana/dbc/dbcfile.cc | 23 +-- tools/cabana/dbc/dbcfile.h | 6 +- tools/cabana/mainwin.cc | 207 +++++++++--------------- tools/cabana/mainwin.h | 17 +- tools/cabana/signalview.cc | 2 +- tools/cabana/signalview.h | 1 + tools/cabana/streams/abstractstream.cc | 11 -- tools/cabana/streams/abstractstream.h | 21 +-- tools/cabana/streams/devicestream.cc | 11 +- tools/cabana/streams/devicestream.h | 6 +- tools/cabana/streams/livestream.cc | 1 - tools/cabana/streams/pandastream.cc | 13 +- tools/cabana/streams/pandastream.h | 6 +- tools/cabana/streams/replaystream.cc | 17 +- tools/cabana/streams/replaystream.h | 8 +- tools/cabana/streams/socketcanstream.cc | 13 +- tools/cabana/streams/socketcanstream.h | 6 +- tools/cabana/streamselector.cc | 16 +- tools/cabana/streamselector.h | 6 +- 25 files changed, 134 insertions(+), 309 deletions(-) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 4a3b4126dd..a798d13a81 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -38,26 +38,19 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); cmd_parser.process(app); - QString dbc_file = cmd_parser.isSet("dbc") ? cmd_parser.value("dbc") : ""; - AbstractStream *stream = nullptr; + if (cmd_parser.isSet("stream")) { stream = new DeviceStream(&app, cmd_parser.value("zmq")); } else if (cmd_parser.isSet("panda") || cmd_parser.isSet("panda-serial")) { - PandaStreamConfig config = {}; - if (cmd_parser.isSet("panda-serial")) { - config.serial = cmd_parser.value("panda-serial"); - } try { - stream = new PandaStream(&app, config); + stream = new PandaStream(&app, {.serial = cmd_parser.value("panda-serial")}); } catch (std::exception &e) { qWarning() << e.what(); return 0; } } else if (cmd_parser.isSet("socketcan")) { - SocketCanStreamConfig config = {}; - config.device = cmd_parser.value("socketcan"); - stream = new SocketCanStream(&app, config); + stream = new SocketCanStream(&app, {.device = cmd_parser.value("socketcan")}); } else { uint32_t replay_flags = REPLAY_FLAG_NONE; if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; @@ -73,24 +66,14 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } if (!route.isEmpty()) { - auto replay_stream = new ReplayStream(&app); + auto replay_stream = std::make_unique(&app); if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; } - stream = replay_stream; + stream = replay_stream.release(); } } - MainWindow w; - if (stream) { - stream->start(); - if (!dbc_file.isEmpty()) { - w.loadFile(dbc_file); - } - } else { - w.openStream(); - } - w.show(); - + MainWindow w(stream, cmd_parser.value("dbc")); return app.exec(); } diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 128c33baec..6c13252e80 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -155,11 +155,10 @@ void ChartView::removeIf(std::function predicate) { } void ChartView::signalUpdated(const cabana::Signal *sig) { - if (std::any_of(sigs.cbegin(), sigs.cend(), [=](auto &s) { return s.sig == sig; })) { - for (const auto &s : sigs) { - if (s.sig == sig && s.series->color() != sig->color) { - setSeriesColor(s.series, sig->color); - } + auto it = std::find_if(sigs.begin(), sigs.end(), [sig](auto &s) { return s.sig == sig; }); + if (it != sigs.end()) { + if (it->series->color() != sig->color) { + setSeriesColor(it->series, sig->color); } updateTitle(); updateSeries(sig); diff --git a/tools/cabana/chart/chartswidget.h b/tools/cabana/chart/chartswidget.h index 0b948b3421..a925191792 100644 --- a/tools/cabana/chart/chartswidget.h +++ b/tools/cabana/chart/chartswidget.h @@ -110,7 +110,6 @@ private: QTimer *align_timer; int current_theme = 0; bool value_tip_visible_ = false; - friend class ZoomCommand; friend class ChartView; friend class ChartsContainer; }; diff --git a/tools/cabana/chart/signalselector.cc b/tools/cabana/chart/signalselector.cc index 0ddb212a8a..63f3a7d575 100644 --- a/tools/cabana/chart/signalselector.cc +++ b/tools/cabana/chart/signalselector.cc @@ -83,7 +83,8 @@ void SignalSelector::updateAvailableList(int index) { MessageId msg_id = msgs_combo->itemData(index).value(); auto selected_items = seletedItems(); for (auto s : dbc()->msg(msg_id)->getSignals()) { - bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), [=, sig = s](auto it) { return it->msg_id == msg_id && it->sig == sig; }); + bool is_selected = std::any_of(selected_items.begin(), selected_items.end(), + [sig = s, &msg_id](auto it) { return it->msg_id == msg_id && it->sig == sig; }); if (!is_selected) { addItemToList(available_list, msg_id, s); } diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 1cadda2bd8..094fe96182 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -4,8 +4,6 @@ #include #include -#include "tools/cabana/streams/abstractstream.h" - void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { const auto &msgs = can->events(msg_id); @@ -14,11 +12,6 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl auto first = std::lower_bound(msgs.cbegin(), msgs.cend(), range_start, CompareCanEvent()); auto last = std::upper_bound(first, msgs.cend(), range_end, CompareCanEvent()); - if (first == last || size.isEmpty()) { - pixmap = QPixmap(); - return; - } - points.clear(); double value = 0; for (auto it = first; it != last; ++it) { @@ -27,7 +20,7 @@ void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, doubl } } - if (points.empty()) { + if (points.empty() || size.isEmpty()) { pixmap = QPixmap(); return; } diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h index 3bdd8a3ee5..806f0a61eb 100644 --- a/tools/cabana/chart/sparkline.h +++ b/tools/cabana/chart/sparkline.h @@ -5,6 +5,7 @@ #include #include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/streams/abstractstream.h" class Sparkline { public: diff --git a/tools/cabana/dbc/dbcfile.cc b/tools/cabana/dbc/dbcfile.cc index 4a1c52819a..cca9d0c0fa 100644 --- a/tools/cabana/dbc/dbcfile.cc +++ b/tools/cabana/dbc/dbcfile.cc @@ -9,10 +9,6 @@ DBCFile::DBCFile(const QString &dbc_file_name) { if (file.open(QIODevice::ReadOnly)) { name_ = QFileInfo(dbc_file_name).baseName(); filename = dbc_file_name; - // Remove auto save file extension - if (dbc_file_name.endsWith(AUTO_SAVE_EXTENSION)) { - filename.chop(AUTO_SAVE_EXTENSION.length()); - } parse(file.readAll()); } else { throw std::runtime_error("Failed to open file."); @@ -20,17 +16,12 @@ DBCFile::DBCFile(const QString &dbc_file_name) { } DBCFile::DBCFile(const QString &name, const QString &content) : name_(name), filename("") { - // Open from clipboard parse(content); } bool DBCFile::save() { assert(!filename.isEmpty()); - if (writeContents(filename)) { - cleanupAutoSaveFile(); - return true; - } - return false; + return writeContents(filename); } bool DBCFile::saveAs(const QString &new_filename) { @@ -38,16 +29,6 @@ bool DBCFile::saveAs(const QString &new_filename) { return save(); } -bool DBCFile::autoSave() { - return !filename.isEmpty() && writeContents(filename + AUTO_SAVE_EXTENSION); -} - -void DBCFile::cleanupAutoSaveFile() { - if (!filename.isEmpty()) { - QFile::remove(filename + AUTO_SAVE_EXTENSION); - } -} - bool DBCFile::writeContents(const QString &fn) { QFile file(fn); if (file.open(QIODevice::WriteOnly)) { @@ -75,7 +56,7 @@ cabana::Msg *DBCFile::msg(const QString &name) { return it != msgs.end() ? &(it->second) : nullptr; } -cabana::Signal *DBCFile::signal(uint32_t address, const QString name) { +cabana::Signal *DBCFile::signal(uint32_t address, const QString &name) { auto m = msg(address); return m ? (cabana::Signal *)m->sig(name) : nullptr; } diff --git a/tools/cabana/dbc/dbcfile.h b/tools/cabana/dbc/dbcfile.h index 20de04a861..bd267898f9 100644 --- a/tools/cabana/dbc/dbcfile.h +++ b/tools/cabana/dbc/dbcfile.h @@ -5,8 +5,6 @@ #include "tools/cabana/dbc/dbc.h" -const QString AUTO_SAVE_EXTENSION = ".tmp"; - class DBCFile { public: DBCFile(const QString &dbc_file_name); @@ -15,9 +13,7 @@ public: bool save(); bool saveAs(const QString &new_filename); - bool autoSave(); bool writeContents(const QString &fn); - void cleanupAutoSaveFile(); QString generateDBC(); void updateMsg(const MessageId &id, const QString &name, uint32_t size, const QString &node, const QString &comment); @@ -27,7 +23,7 @@ public: cabana::Msg *msg(uint32_t address); cabana::Msg *msg(const QString &name); inline cabana::Msg *msg(const MessageId &id) { return msg(id.address); } - cabana::Signal *signal(uint32_t address, const QString name); + cabana::Signal *signal(uint32_t address, const QString &name); inline QString name() const { return name_.isEmpty() ? "untitled" : name_; } inline bool isEmpty() const { return msgs.empty() && name_.isEmpty(); } diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index bf1db63eea..34432707db 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -23,9 +24,8 @@ #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" -#include "tools/replay/replay.h" -MainWindow::MainWindow() : QMainWindow() { +MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainWindow() { loadFingerprints(); createDockWindows(); setCentralWidget(center_widget = new CenterWidget(this)); @@ -65,10 +65,10 @@ MainWindow::MainWindow() : QMainWindow() { QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &MainWindow::DBCFileChanged); QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged); - QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &MainWindow::undoStackIndexChanged); QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); - QObject::connect(StreamNotifier::instance(), &StreamNotifier::changingStream, this, &MainWindow::changingStream); - QObject::connect(StreamNotifier::instance(), &StreamNotifier::streamStarted, this, &MainWindow::streamStarted); + + QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); + show(); } void MainWindow::loadFingerprints() { @@ -76,16 +76,12 @@ void MainWindow::loadFingerprints() { if (json_file.open(QIODevice::ReadOnly)) { fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); } - // get opendbc names - for (auto fn : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) { - opendbc_names << QFileInfo(fn).baseName(); - } } void MainWindow::createActions() { // File menu QMenu *file_menu = menuBar()->addMenu(tr("&File")); - file_menu->addAction(tr("Open Stream..."), this, &MainWindow::openStream); + file_menu->addAction(tr("Open Stream..."), this, &MainWindow::selectAndOpenStream); close_stream_act = file_menu->addAction(tr("Close stream"), this, &MainWindow::closeStream); export_to_csv_act = file_menu->addAction(tr("Export to CSV..."), this, &MainWindow::exportToCSV); close_stream_act->setEnabled(false); @@ -96,20 +92,15 @@ void MainWindow::createActions() { file_menu->addAction(tr("Open DBC File..."), [this]() { openFile(); }, QKeySequence::Open); manage_dbcs_menu = file_menu->addMenu(tr("Manage &DBC Files")); + QObject::connect(manage_dbcs_menu, &QMenu::aboutToShow, this, &MainWindow::updateLoadSaveMenus); open_recent_menu = file_menu->addMenu(tr("Open &Recent")); - for (int i = 0; i < MAX_RECENT_FILES; ++i) { - recent_files_acts[i] = new QAction(this); - recent_files_acts[i]->setVisible(false); - QObject::connect(recent_files_acts[i], &QAction::triggered, this, &MainWindow::openRecentFile); - open_recent_menu->addAction(recent_files_acts[i]); - } - updateRecentFileActions(); + QObject::connect(open_recent_menu, &QMenu::aboutToShow, this, &MainWindow::updateRecentFileMenu); file_menu->addSeparator(); QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc")); // load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }"); - for (const auto &dbc_name : opendbc_names) { + for (const auto &dbc_name : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) { load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); }); } @@ -180,6 +171,7 @@ void MainWindow::createDockWidgets() { messages_widget = new MessagesWidget(this); messages_dock->setWidget(messages_widget); QObject::connect(messages_widget, &MessagesWidget::titleChanged, messages_dock, &QDockWidget::setWindowTitle); + QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage); // right panel charts_widget = new ChartsWidget(this); @@ -209,67 +201,52 @@ void MainWindow::createStatusBar() { progress_bar->setVisible(false); statusBar()->addWidget(new QLabel(tr("For Help, Press F1"))); statusBar()->addPermanentWidget(progress_bar); - statusBar()->addPermanentWidget(status_label = new QLabel(this)); updateStatus(); } void MainWindow::createShortcuts() { auto shortcut = new QShortcut(QKeySequence(Qt::Key_Space), this, nullptr, nullptr, Qt::ApplicationShortcut); - QObject::connect(shortcut, &QShortcut::activated, []() { can->pause(!can->isPaused()); }); + QObject::connect(shortcut, &QShortcut::activated, this, []() { + if (can) can->pause(!can->isPaused()); + }); // TODO: add more shortcuts here. } -void MainWindow::undoStackIndexChanged(int index) { - int count = UndoStack::instance()->count(); - if (count >= 0) { - QString command_text; - if (index == count) { - command_text = (count == prev_undostack_count ? "Redo " : "") + UndoStack::instance()->text(index - 1); - } else if (index < prev_undostack_index) { - command_text = tr("Undo %1").arg(UndoStack::instance()->text(index)); - } else if (index > prev_undostack_index) { - command_text = tr("Redo %1").arg(UndoStack::instance()->text(index - 1)); - } - statusBar()->showMessage(command_text, 2000); - } - prev_undostack_index = index; - prev_undostack_count = count; - autoSave(); - updateLoadSaveMenus(); -} - void MainWindow::undoStackCleanChanged(bool clean) { - if (clean) { - prev_undostack_index = 0; - prev_undostack_count = 0; - } setWindowModified(!clean); } void MainWindow::DBCFileChanged() { UndoStack::instance()->clear(); - updateLoadSaveMenus(); + + // Update file menu + int cnt = dbc()->nonEmptyDBCCount(); + save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC...")); + save_dbc->setEnabled(cnt > 0); + save_dbc_as->setEnabled(cnt == 1); + // TODO: Support clipboard for multiple files + copy_dbc_to_clipboard->setEnabled(cnt == 1); + manage_dbcs_menu->setEnabled(dynamic_cast(can) == nullptr); + + QStringList title; + for (auto f : dbc()->allDBCFiles()) { + title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name())); + } + setWindowFilePath(title.join(" | ")); } -void MainWindow::openStream() { - AbstractStream *stream = nullptr; - StreamSelector dlg(&stream, this); +void MainWindow::selectAndOpenStream() { + StreamSelector dlg(this); if (dlg.exec()) { - if (!dlg.dbcFile().isEmpty()) { - loadFile(dlg.dbcFile()); - } - stream->start(); - statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000); + openStream(dlg.stream(), dlg.dbcFile()); } else if (!can) { - stream = new DummyStream(this); - stream->start(); + openStream(new DummyStream(this)); } } void MainWindow::closeStream() { - AbstractStream *stream = new DummyStream(this); - stream->start(); + openStream(new DummyStream(this)); if (dbc()->nonEmptyDBCCount() > 0) { emit dbc()->DBCFileChanged(); } @@ -301,18 +278,8 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) { if (!fn.isEmpty()) { closeFile(s); - QString dbc_fn = fn; - // Prompt user to load auto saved file if it exists. - if (QFile::exists(fn + AUTO_SAVE_EXTENSION)) { - auto ret = QMessageBox::question(this, tr("Auto saved DBC found"), tr("Auto saved DBC file from previous session found. Do you want to load it instead?")); - if (ret == QMessageBox::Yes) { - dbc_fn += AUTO_SAVE_EXTENSION; - UndoStack::instance()->resetClean(); // Force user to save on close so the auto saved file is not lost - } - } - QString error; - if (dbc()->open(s, dbc_fn, &error)) { + if (dbc()->open(s, fn, &error)) { updateRecentFiles(fn); statusBar()->showMessage(tr("DBC File %1 loaded").arg(fn), 2000); } else { @@ -323,15 +290,8 @@ void MainWindow::loadFile(const QString &fn, SourceSet s) { } } -void MainWindow::openRecentFile() { - if (auto action = qobject_cast(sender())) { - loadFile(action->data().toString()); - } -} - void MainWindow::loadDBCFromOpendbc(const QString &name) { - QString opendbc_file_path = QString("%1/%2.dbc").arg(OPENDBC_FILE_PATH, name); - loadFile(opendbc_file_path); + loadFile(QString("%1/%2").arg(OPENDBC_FILE_PATH, name)); } void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { @@ -349,13 +309,19 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { } } -void MainWindow::changingStream() { +void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) { center_widget->clear(); delete messages_widget; delete video_splitter; -} -void MainWindow::streamStarted() { + delete can; + can = stream; + can->setParent(this); // take ownership + can->start(); + + loadFile(dbc_file); + statusBar()->showMessage(tr("Stream [%1] started").arg(can->routeName()), 2000); + bool has_stream = dynamic_cast(can) == nullptr; close_stream_act->setEnabled(has_stream); export_to_csv_act->setEnabled(has_stream); @@ -372,9 +338,20 @@ void MainWindow::streamStarted() { newFile(); } - QObject::connect(messages_widget, &MessagesWidget::msgSelectionChanged, center_widget, &CenterWidget::setMessage); QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged); - QObject::connect(can, &AbstractStream::sourcesUpdated, this, &MainWindow::updateLoadSaveMenus); + + if (has_stream) { + auto wait_dlg = new QProgressDialog( + can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."), + tr("&Abort"), 0, 100, this); + wait_dlg->setWindowModality(Qt::WindowModal); + wait_dlg->setFixedSize(400, wait_dlg->sizeHint().height()); + QObject::connect(wait_dlg, &QProgressDialog::canceled, this, &MainWindow::close); + QObject::connect(can, &AbstractStream::eventsMerged, wait_dlg, &QProgressDialog::deleteLater); + QObject::connect(this, &MainWindow::updateProgressBar, wait_dlg, [=](uint64_t cur, uint64_t total, bool success) { + wait_dlg->setValue((int)((cur / (double)total) * 100)); + }); + } } void MainWindow::eventsMerged() { @@ -383,12 +360,8 @@ void MainWindow::eventsMerged() { .arg(can->routeName()) .arg(car_fingerprint.isEmpty() ? tr("Unknown Car") : car_fingerprint)); // Don't overwrite already loaded DBC - if (!dbc()->nonEmptyDBCCount() && !car_fingerprint.isEmpty()) { - auto dbc_name = fingerprint_to_dbc[car_fingerprint]; - if (dbc_name != QJsonValue::Undefined) { - // Prevent dialog that load autosaved file from blocking replay->start(). - QTimer::singleShot(0, this, [dbc_name, this]() { loadDBCFromOpendbc(dbc_name.toString()); }); - } + if (!dbc()->nonEmptyDBCCount() && fingerprint_to_dbc.object().contains(car_fingerprint)) { + QTimer::singleShot(0, this, [this]() { loadDBCFromOpendbc(fingerprint_to_dbc[car_fingerprint].toString() + ".dbc"); }); } } } @@ -409,22 +382,6 @@ void MainWindow::saveAs() { } } -void MainWindow::autoSave() { - if (!UndoStack::instance()->isClean()) { - for (auto dbc_file : dbc()->allDBCFiles()) { - if (!dbc_file->filename.isEmpty()) { - dbc_file->autoSave(); - } - } - } -} - -void MainWindow::cleanupAutoSaveFile() { - for (auto dbc_file : dbc()->allDBCFiles()) { - dbc_file->cleanupAutoSaveFile(); - } -} - void MainWindow::closeFile(SourceSet s) { remindSaveChanges(); if (s == SOURCE_ALL) { @@ -448,7 +405,6 @@ void MainWindow::saveFile(DBCFile *dbc_file) { assert(dbc_file != nullptr); if (!dbc_file->filename.isEmpty()) { dbc_file->save(); - updateLoadSaveMenus(); UndoStack::instance()->setClean(); statusBar()->showMessage(tr("File saved"), 2000); } else if (!dbc_file->isEmpty()) { @@ -464,7 +420,6 @@ void MainWindow::saveFileAs(DBCFile *dbc_file) { UndoStack::instance()->setClean(); statusBar()->showMessage(tr("File saved as %1").arg(fn), 2000); updateRecentFiles(fn); - updateLoadSaveMenus(); } } @@ -483,16 +438,7 @@ void MainWindow::saveFileToClipboard(DBCFile *dbc_file) { } void MainWindow::updateLoadSaveMenus() { - int cnt = dbc()->nonEmptyDBCCount(); - save_dbc->setText(cnt > 1 ? tr("Save %1 DBCs...").arg(cnt) : tr("Save DBC...")); - save_dbc->setEnabled(cnt > 0); - save_dbc_as->setEnabled(cnt == 1); - - // TODO: Support clipboard for multiple files - copy_dbc_to_clipboard->setEnabled(cnt == 1); - manage_dbcs_menu->clear(); - manage_dbcs_menu->setEnabled(dynamic_cast(can) == nullptr); for (int source : can->sources) { if (source >= 64) continue; // Sent and blocked buses are handled implicitly @@ -505,8 +451,8 @@ void MainWindow::updateLoadSaveMenus() { bus_menu->addAction(tr("Load DBC From Clipboard..."), [=]() { loadFromClipboard(ss, false); }); // Show sub-menu for each dbc for this source. - QString file_name = "No DBCs loaded"; - if (auto dbc_file = dbc()->findDBCFile(source)) { + auto dbc_file = dbc()->findDBCFile(source); + if (dbc_file) { bus_menu->addSeparator(); bus_menu->addAction(dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")")->setEnabled(false); bus_menu->addAction(tr("Save..."), [=]() { saveFile(dbc_file); }); @@ -514,19 +460,11 @@ void MainWindow::updateLoadSaveMenus() { bus_menu->addAction(tr("Copy to Clipboard..."), [=]() { saveFileToClipboard(dbc_file); }); bus_menu->addAction(tr("Remove from this bus..."), [=]() { closeFile(ss); }); bus_menu->addAction(tr("Remove from all buses..."), [=]() { closeFile(dbc_file); }); - - file_name = dbc_file->name(); } + bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(dbc_file ? dbc_file->name() : "No DBCs loaded")); manage_dbcs_menu->addMenu(bus_menu); - bus_menu->setTitle(tr("Bus %1 (%2)").arg(source).arg(file_name)); - } - - QStringList title; - for (auto f : dbc()->allDBCFiles()) { - title.push_back(tr("(%1) %2").arg(toString(dbc()->sources(f)), f->name())); } - setWindowFilePath(title.join(" | ")); } void MainWindow::updateRecentFiles(const QString &fn) { @@ -536,21 +474,21 @@ void MainWindow::updateRecentFiles(const QString &fn) { settings.recent_files.removeLast(); } settings.last_dir = QFileInfo(fn).absolutePath(); - updateRecentFileActions(); } -void MainWindow::updateRecentFileActions() { +void MainWindow::updateRecentFileMenu() { + open_recent_menu->clear(); + int num_recent_files = std::min(settings.recent_files.size(), MAX_RECENT_FILES); + if (!num_recent_files) { + open_recent_menu->addAction(tr("No Recent Files"))->setEnabled(false); + return; + } + for (int i = 0; i < num_recent_files; ++i) { QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(settings.recent_files[i]).fileName()); - recent_files_acts[i]->setText(text); - recent_files_acts[i]->setData(settings.recent_files[i]); - recent_files_acts[i]->setVisible(true); - } - for (int i = num_recent_files; i < MAX_RECENT_FILES; ++i) { - recent_files_acts[i]->setVisible(false); + open_recent_menu->addAction(text, this, [this, i=i](){ loadFile(settings.recent_files[i]); }); } - open_recent_menu->setEnabled(num_recent_files > 0); } void MainWindow::remindSaveChanges() { @@ -606,7 +544,6 @@ void MainWindow::toggleChartsDocking() { } void MainWindow::closeEvent(QCloseEvent *event) { - cleanupAutoSaveFile(); remindSaveChanges(); installDownloadProgressHandler(nullptr); @@ -618,7 +555,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { // save states settings.geometry = saveGeometry(); settings.window_state = saveState(); - if (!can->liveStreaming()) { + if (can && !can->liveStreaming()) { settings.video_splitter_state = video_splitter->saveState(); } settings.message_header_state = messages_widget->saveHeaderState(); diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 77add4d789..9bc94c090f 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -20,22 +20,20 @@ class MainWindow : public QMainWindow { Q_OBJECT public: - MainWindow(); + MainWindow(AbstractStream *stream, const QString &dbc_file); void toggleChartsDocking(); void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } void loadFile(const QString &fn, SourceSet s = SOURCE_ALL); ChartsWidget *charts_widget = nullptr; public slots: - void openStream(); + void selectAndOpenStream(); + void openStream(AbstractStream *stream, const QString &dbc_file = {}); void closeStream(); void exportToCSV(); - void changingStream(); - void streamStarted(); void newFile(SourceSet s = SOURCE_ALL); void openFile(SourceSet s = SOURCE_ALL); - void openRecentFile(); void loadDBCFromOpendbc(const QString &name); void save(); void saveAs(); @@ -55,10 +53,8 @@ protected: void saveFileToClipboard(DBCFile *dbc_file); void loadFingerprints(); void loadFromClipboard(SourceSet s = SOURCE_ALL, bool close_all = true); - void autoSave(); - void cleanupAutoSaveFile(); void updateRecentFiles(const QString &fn); - void updateRecentFileActions(); + void updateRecentFileMenu(); void createActions(); void createDockWindows(); void createStatusBar(); @@ -70,7 +66,6 @@ protected: void findSimilarBits(); void findSignal(); void undoStackCleanChanged(bool clean); - void undoStackIndexChanged(int index); void onlineHelp(); void toggleFullScreen(); void updateStatus(); @@ -88,10 +83,8 @@ protected: QProgressBar *progress_bar; QLabel *status_label; QJsonDocument fingerprint_to_dbc; - QStringList opendbc_names; QSplitter *video_splitter = nullptr; enum { MAX_RECENT_FILES = 15 }; - QAction *recent_files_acts[MAX_RECENT_FILES] = {}; QMenu *open_recent_menu = nullptr; QMenu *manage_dbcs_menu = nullptr; QMenu *tools_menu = nullptr; @@ -101,8 +94,6 @@ protected: QAction *save_dbc_as = nullptr; QAction *copy_dbc_to_clipboard = nullptr; QString car_fingerprint; - int prev_undostack_index = 0; - int prev_undostack_count = 0; QByteArray default_state; }; diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 8288c801b5..130b76d502 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -494,6 +494,7 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), } void SignalView::setMessage(const MessageId &id) { + max_value_width = 0; filter_edit->clear(); model->setMessage(id); } @@ -617,7 +618,6 @@ void SignalView::updateState(const std::set *msgs) { const auto &last_msg = can->lastMessage(model->msg_id); if (model->rowCount() == 0 || (msgs && !msgs->count(model->msg_id)) || last_msg.dat.size() == 0) return; - int max_value_width = 0; for (auto item : model->root->children) { double value = 0; if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) { diff --git a/tools/cabana/signalview.h b/tools/cabana/signalview.h index 6320b9b621..4e746ea105 100644 --- a/tools/cabana/signalview.h +++ b/tools/cabana/signalview.h @@ -135,6 +135,7 @@ private: QTreeView::leaveEvent(event); } }; + int max_value_width = 0; int value_column_width = 0; TreeView *tree; QLabel *sparkline_label; diff --git a/tools/cabana/streams/abstractstream.cc b/tools/cabana/streams/abstractstream.cc index ee6a1143c8..9bf80deb98 100644 --- a/tools/cabana/streams/abstractstream.cc +++ b/tools/cabana/streams/abstractstream.cc @@ -10,11 +10,6 @@ static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB AbstractStream *can = nullptr; -StreamNotifier *StreamNotifier::instance() { - static StreamNotifier notifier; - return ¬ifier; -} - AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { assert(parent != nullptr); event_buffer_ = std::make_unique(EVENT_NEXT_BUFFER_SIZE); @@ -24,12 +19,6 @@ AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); QObject::connect(dbc(), &DBCManager::DBCFileChanged, this, &AbstractStream::updateMasks); QObject::connect(dbc(), &DBCManager::maskUpdated, this, &AbstractStream::updateMasks); - QObject::connect(this, &AbstractStream::streamStarted, [this]() { - emit StreamNotifier::instance()->changingStream(); - delete can; - can = this; - emit StreamNotifier::instance()->streamStarted(); - }); } void AbstractStream::updateMasks() { diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 9e3f92dc5d..7ae119bcf0 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -96,7 +96,6 @@ signals: void seeking(double sec); void seekedTo(double sec); void timeRangeChanged(const std::optional> &range); - void streamStarted(); void eventsMerged(const MessageEventsMap &events_map); void msgsReceived(const std::set *new_msgs, bool has_new_ids); void sourcesUpdated(const SourceSet &s); @@ -133,15 +132,11 @@ private: class AbstractOpenStreamWidget : public QWidget { Q_OBJECT public: - AbstractOpenStreamWidget(AbstractStream **stream, QWidget *parent = nullptr) : stream(stream), QWidget(parent) {} - virtual bool open() = 0; - virtual QString title() = 0; + AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} + virtual AbstractStream *open() = 0; signals: void enableOpenButton(bool); - -protected: - AbstractStream **stream = nullptr; }; class DummyStream : public AbstractStream { @@ -149,17 +144,7 @@ class DummyStream : public AbstractStream { public: DummyStream(QObject *parent) : AbstractStream(parent) {} QString routeName() const override { return tr("No Stream"); } - void start() override { emit streamStarted(); } -}; - -class StreamNotifier : public QObject { - Q_OBJECT -public: - StreamNotifier(QObject *parent = nullptr) : QObject(parent) {} - static StreamNotifier* instance(); -signals: - void streamStarted(); - void changingStream(); + void start() override {} }; // A global pointer referring to the unique AbstractStream object diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 80507391a7..6de63dfbbc 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -33,13 +33,9 @@ void DeviceStream::streamThread() { } } -AbstractOpenStreamWidget *DeviceStream::widget(AbstractStream **stream) { - return new OpenDeviceWidget(stream); -} - // OpenDeviceWidget -OpenDeviceWidget::OpenDeviceWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { +OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { QRadioButton *msgq = new QRadioButton(tr("MSGQ")); QRadioButton *zmq = new QRadioButton(tr("ZMQ")); ip_address = new QLineEdit(this); @@ -62,9 +58,8 @@ OpenDeviceWidget::OpenDeviceWidget(AbstractStream **stream) : AbstractOpenStream zmq->setChecked(true); } -bool OpenDeviceWidget::open() { +AbstractStream *OpenDeviceWidget::open() { QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); bool msgq = group->checkedId() == 0; - *stream = new DeviceStream(qApp, msgq ? "" : ip); - return true; + return new DeviceStream(qApp, msgq ? "" : ip); } diff --git a/tools/cabana/streams/devicestream.h b/tools/cabana/streams/devicestream.h index a65f073458..3bdf224998 100644 --- a/tools/cabana/streams/devicestream.h +++ b/tools/cabana/streams/devicestream.h @@ -6,7 +6,6 @@ class DeviceStream : public LiveStream { Q_OBJECT public: DeviceStream(QObject *parent, QString address = {}); - static AbstractOpenStreamWidget *widget(AbstractStream **stream); inline QString routeName() const override { return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); } @@ -20,9 +19,8 @@ class OpenDeviceWidget : public AbstractOpenStreamWidget { Q_OBJECT public: - OpenDeviceWidget(AbstractStream **stream); - bool open() override; - QString title() override { return tr("&Device"); } + OpenDeviceWidget(QWidget *parent = nullptr); + AbstractStream *open() override; private: QLineEdit *ip_address; diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 6bcb1c1d54..ac9a6fa105 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -53,7 +53,6 @@ void LiveStream::startUpdateTimer() { } void LiveStream::start() { - emit streamStarted(); stream_thread->start(); startUpdateTimer(); begin_date_time = QDateTime::currentDateTime(); diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index 030783a899..cdf6da6d83 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -77,13 +77,9 @@ void PandaStream::streamThread() { } } -AbstractOpenStreamWidget *PandaStream::widget(AbstractStream **stream) { - return new OpenPandaWidget(stream); -} - // OpenPandaWidget -OpenPandaWidget::OpenPandaWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { +OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { form_layout = new QFormLayout(this); if (can && dynamic_cast(can) != nullptr) { form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(can->routeName()))); @@ -182,12 +178,11 @@ void OpenPandaWidget::buildConfigForm() { } } -bool OpenPandaWidget::open() { +AbstractStream *OpenPandaWidget::open() { try { - *stream = new PandaStream(qApp, config); - return true; + return new PandaStream(qApp, config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); - return false; + return nullptr; } } diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index ad792ec292..826b1aa986 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -28,7 +28,6 @@ class PandaStream : public LiveStream { public: PandaStream(QObject *parent, PandaStreamConfig config_ = {}); ~PandaStream() { stop(); } - static AbstractOpenStreamWidget *widget(AbstractStream **stream); inline QString routeName() const override { return QString("Panda: %1").arg(config.serial); } @@ -45,9 +44,8 @@ class OpenPandaWidget : public AbstractOpenStreamWidget { Q_OBJECT public: - OpenPandaWidget(AbstractStream **stream); - bool open() override; - QString title() override { return tr("&Panda"); } + OpenPandaWidget(QWidget *parent = nullptr); + AbstractStream *open() override; private: void refreshSerials(); diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 09ba3db417..1616f2aec3 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -83,11 +83,6 @@ bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint return success; } -void ReplayStream::start() { - emit streamStarted(); - replay->start(); -} - bool ReplayStream::eventFilter(const Event *event) { static double prev_update_ts = 0; if (event->which == cereal::Event::Which::CAN) { @@ -115,13 +110,9 @@ void ReplayStream::pause(bool pause) { } -AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { - return new OpenReplayWidget(stream); -} - // OpenReplayWidget -OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { +OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { QGridLayout *grid_layout = new QGridLayout(this); grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); @@ -154,7 +145,7 @@ OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStream }); } -bool OpenReplayWidget::open() { +AbstractStream *OpenReplayWidget::open() { QString route = route_edit->text(); QString data_dir; if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { @@ -173,8 +164,8 @@ bool OpenReplayWidget::open() { if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; if (replay_stream->loadRoute(route, data_dir, flags)) { - *stream = replay_stream.release(); + return replay_stream.release(); } } - return *stream != nullptr; + return nullptr; } diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index 217d1ac1d3..25d6ef4a72 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -15,7 +15,7 @@ class ReplayStream : public AbstractStream { public: ReplayStream(QObject *parent); - void start() override; + void start() override { replay->start(); } bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); bool eventFilter(const Event *event); void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); } @@ -31,7 +31,6 @@ public: inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; - static AbstractOpenStreamWidget *widget(AbstractStream **stream); private: void mergeSegments(); @@ -44,9 +43,8 @@ class OpenReplayWidget : public AbstractOpenStreamWidget { Q_OBJECT public: - OpenReplayWidget(AbstractStream **stream); - bool open() override; - QString title() override { return tr("&Replay"); } + OpenReplayWidget(QWidget *parent = nullptr); + AbstractStream *open() override; private: QLineEdit *route_edit; diff --git a/tools/cabana/streams/socketcanstream.cc b/tools/cabana/streams/socketcanstream.cc index 0f13b9901b..1346a496bb 100644 --- a/tools/cabana/streams/socketcanstream.cc +++ b/tools/cabana/streams/socketcanstream.cc @@ -66,11 +66,7 @@ void SocketCanStream::streamThread() { } } -AbstractOpenStreamWidget *SocketCanStream::widget(AbstractStream **stream) { - return new OpenSocketCanWidget(stream); -} - -OpenSocketCanWidget::OpenSocketCanWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { +OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->addStretch(1); @@ -104,12 +100,11 @@ void OpenSocketCanWidget::refreshDevices() { } -bool OpenSocketCanWidget::open() { +AbstractStream *OpenSocketCanWidget::open() { try { - *stream = new SocketCanStream(qApp, config); + return new SocketCanStream(qApp, config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); - return false; + return nullptr; } - return true; } diff --git a/tools/cabana/streams/socketcanstream.h b/tools/cabana/streams/socketcanstream.h index 952f1aaa5c..8083b687e9 100644 --- a/tools/cabana/streams/socketcanstream.h +++ b/tools/cabana/streams/socketcanstream.h @@ -18,7 +18,6 @@ class SocketCanStream : public LiveStream { public: SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {}); ~SocketCanStream() { stop(); } - static AbstractOpenStreamWidget *widget(AbstractStream **stream); static bool available(); inline QString routeName() const override { @@ -37,9 +36,8 @@ class OpenSocketCanWidget : public AbstractOpenStreamWidget { Q_OBJECT public: - OpenSocketCanWidget(AbstractStream **stream); - bool open() override; - QString title() override { return tr("&SocketCAN"); } + OpenSocketCanWidget(QWidget *parent = nullptr); + AbstractStream *open() override; private: void refreshDevices(); diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc index 209b577c9a..efd00d3985 100644 --- a/tools/cabana/streamselector.cc +++ b/tools/cabana/streamselector.cc @@ -10,7 +10,7 @@ #include "tools/cabana/streams/replaystream.h" #include "tools/cabana/streams/socketcanstream.h" -StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) { +StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Open stream")); QVBoxLayout *layout = new QVBoxLayout(this); tab = new QTabWidget(this); @@ -33,17 +33,17 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); layout->addWidget(btn_box); - addStreamWidget(ReplayStream::widget(stream)); - addStreamWidget(PandaStream::widget(stream)); + addStreamWidget(new OpenReplayWidget, tr("&Replay")); + addStreamWidget(new OpenPandaWidget, tr("&Panda")); if (SocketCanStream::available()) { - addStreamWidget(SocketCanStream::widget(stream)); + addStreamWidget(new OpenSocketCanWidget, tr("&SocketCAN")); } - addStreamWidget(DeviceStream::widget(stream)); + addStreamWidget(new OpenDeviceWidget, tr("&Device")); QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { setEnabled(false); - if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) { + if (stream_ = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); stream_) { accept(); } setEnabled(true); @@ -57,8 +57,8 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial }); } -void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w) { - tab->addTab(w, w->title()); +void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w, const QString &title) { + tab->addTab(w, title); auto open_btn = btn_box->button(QDialogButtonBox::Open); QObject::connect(w, &AbstractOpenStreamWidget::enableOpenButton, open_btn, &QPushButton::setEnabled); } diff --git a/tools/cabana/streamselector.h b/tools/cabana/streamselector.h index a702fc76ad..0919195e4e 100644 --- a/tools/cabana/streamselector.h +++ b/tools/cabana/streamselector.h @@ -11,11 +11,13 @@ class StreamSelector : public QDialog { Q_OBJECT public: - StreamSelector(AbstractStream **stream, QWidget *parent = nullptr); - void addStreamWidget(AbstractOpenStreamWidget *w); + StreamSelector(QWidget *parent = nullptr); + void addStreamWidget(AbstractOpenStreamWidget *w, const QString &title); QString dbcFile() const { return dbc_file->text(); } + AbstractStream *stream() const { return stream_; } private: + AbstractStream *stream_ = nullptr; QLineEdit *dbc_file; QTabWidget *tab; QDialogButtonBox *btn_box;