diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index f7db3f23b7..63b725a759 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -100,7 +100,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"] cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/qtutil.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] @@ -120,6 +120,8 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), + dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'), + dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src), dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), ] dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 160aead6fd..20fd2ec1ec 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -13,6 +13,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/utils/qtutil.h" // BinaryView diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index 0b0159b422..c918ee01e7 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 6cc6046bec..b18484c556 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -12,6 +12,7 @@ #ifdef __linux__ #include "tools/cabana/streams/socketcanstream.h" #endif +#include "tools/cabana/utils/qtutil.h" namespace { @@ -134,7 +135,14 @@ int main(int argc, char *argv[]) { app.setApplicationDisplayName("Cabana"); //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui - UnixSignalHandler signalHandler; + // Marshal exit onto the GUI thread (qApp methods are not thread-safe). + UnixSignalHandler signalHandler([]() { + QMetaObject::invokeMethod(qApp, []() { + printf("\nexiting...\n"); + qApp->closeAllWindows(); + qApp->exit(); + }, Qt::QueuedConnection); + }); utils::setTheme(settings.theme); CabanaArgs args; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index beece12352..478ff3ce9d 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -248,7 +248,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * } if (!can->liveStreaming()) { - s.segment_tree.build(s.vals); + s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y(); }); } } } diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index d7227b6cf8..3188e1f401 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -10,6 +10,7 @@ #include "tools/cabana/chart/tiplabel.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" enum class SeriesType { Line = 0, diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index 4c143242b2..17e18a90d2 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -13,6 +13,7 @@ #include "tools/cabana/commands.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" const int CHART_MIN_WIDTH = 300; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index aab38f5372..90825dc402 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -8,6 +8,7 @@ #include #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { setWindowTitle(title); diff --git a/openpilot/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc index f5bef0fc2e..587a35956d 100644 --- a/openpilot/tools/cabana/chart/sparkline.cc +++ b/openpilot/tools/cabana/chart/sparkline.cc @@ -3,6 +3,7 @@ #include #include #include +#include "tools/cabana/utils/qtutil.h" void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { if (first == last || size.isEmpty()) { diff --git a/openpilot/tools/cabana/chart/tiplabel.cc b/openpilot/tools/cabana/chart/tiplabel.cc index 233fa80373..c250ebf877 100644 --- a/openpilot/tools/cabana/chart/tiplabel.cc +++ b/openpilot/tools/cabana/chart/tiplabel.cc @@ -7,7 +7,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_ShowWithoutActivating); diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index c6cdd9b1b7..40d1c5ba18 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -1,5 +1,6 @@ #include "tools/cabana/commands.h" +#include #include // UndoStack diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h index 48d320c40f..c003548da4 100644 --- a/openpilot/tools/cabana/detailwidget.h +++ b/openpilot/tools/cabana/detailwidget.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,6 +12,7 @@ #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" #include "tools/cabana/utils/elidedlabel.h" +#include "tools/cabana/utils/qtutil.h" class EditMessageDialog : public QDialog { public: diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 405fe60fbb..1b75e3b8a9 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -10,6 +10,7 @@ #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class HeaderView : public QHeaderView { public: diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 939aade269..2c19df530c 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -23,6 +23,7 @@ #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 28cab25463..7c9fc0253f 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class MessageListModel : public QAbstractTableModel { Q_OBJECT diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc index 3a49de5746..5dd8b7617c 100644 --- a/openpilot/tools/cabana/settingsdialog.cc +++ b/openpilot/tools/cabana/settingsdialog.cc @@ -9,7 +9,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_CACHE_MINIUTES = 30; const int MAX_CACHE_MINIUTES = 120; diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 3a6d4dab25..de80ce2a3f 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -11,10 +11,11 @@ #include #include #include +#include #include #include "tools/cabana/commands.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" // SignalModel diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index cc8b2f96ad..7be579269d 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/abstractstream.h" +#include #include #include diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3a338c7f59..986ca13558 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/devicestream.h" +#include #include #include #include diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 71b5d9a1ec..ec71a6794d 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -9,6 +9,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" struct LiveStream::Logger { Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {} diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 81ed34e8db..944cb10fd9 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -4,6 +4,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 8c95bf584b..31532737f3 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -14,6 +14,7 @@ #include "tools/cabana/streams/devicestream.h" #include "tools/cabana/streams/replaystream.h" #include "tools/cabana/routesdialog.h" +#include "tools/cabana/utils/qtutil.h" // OpenReplayWidget diff --git a/openpilot/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc index 4511af7c01..2e533741cc 100644 --- a/openpilot/tools/cabana/tools/findsignal.cc +++ b/openpilot/tools/cabana/tools/findsignal.cc @@ -10,6 +10,8 @@ #include #include +#include "tools/cabana/utils/qtutil.h" + // FindSignalModel QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/openpilot/tools/cabana/tools/findsignal.h b/openpilot/tools/cabana/tools/findsignal.h index 239a08c9c4..b7cae73b49 100644 --- a/openpilot/tools/cabana/tools/findsignal.h +++ b/openpilot/tools/cabana/tools/findsignal.h @@ -7,8 +7,13 @@ #include #include +#include +#include +#include #include +#include #include +#include #include #include "tools/cabana/commands.h" diff --git a/openpilot/tools/cabana/utils/qtutil.cc b/openpilot/tools/cabana/utils/qtutil.cc new file mode 100644 index 0000000000..040c90c624 --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.cc @@ -0,0 +1,236 @@ +#include "tools/cabana/utils/qtutil.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// MessageBytesDelegate + +MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) + : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { + fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); + for (int i = 0; i < 256; ++i) { + hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); + hex_text_table[i].prepare({}, fixed_font); + } + h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; +} + +QSize MessageBytesDelegate::sizeForBytes(int n) const { + int rows = multiple_lines ? std::max(1, n / 8) : 1; + return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; +} + +QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto data = index.data(BytesRole); + return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); +} + +void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); + } + + QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); + QColor highlighted_color = option.palette.color(QPalette::HighlightedText); + auto text_color = index.data(Qt::ForegroundRole).value(); + bool inactive = text_color.isValid(); + if (!inactive) { + text_color = option.palette.color(QPalette::Text); + } + auto data = index.data(BytesRole); + if (!data.isValid()) { + painter->setFont(option.font); + painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); + painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); + return; + } + + // Paint hex column + const auto &bytes = *static_cast *>(data.value()); + const auto &colors = *static_cast *>(index.data(ColorsRole).value()); + + painter->setFont(fixed_font); + const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + const QPoint pt = item_rect.topLeft(); + for (int i = 0; i < bytes.size(); ++i) { + int row = !multiple_lines ? 0 : i / 8; + int column = !multiple_lines ? i : i % 8; + QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); + + 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, toQColor(colors[i])); + } else { + painter->setPen(text_pen); + } + utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); + } +} + +// TabBar + +int TabBar::addTab(const QString &text) { + int index = QTabBar::addTab(text); + QToolButton *btn = new ToolButton("x", tr("Close Tab")); + int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); + int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); + btn->setFixedSize({width, height}); + setTabButton(index, QTabBar::RightSide, btn); + QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); + return index; +} + +void TabBar::closeTabClicked() { + QObject *object = sender(); + for (int i = 0; i < count(); ++i) { + if (tabButton(i, QTabBar::RightSide) == object) { + emit tabCloseRequested(i); + break; + } + } +} + +// validators + +static QValidator::State toQtState(ValidState s) { + switch (s) { + case ValidState::Acceptable: return QValidator::Acceptable; + case ValidState::Intermediate: return QValidator::Intermediate; + default: return QValidator::Invalid; + } +} + +QValidator::State NameValidator::validate(QString &input, int &pos) const { + std::string s = input.toStdString(); + auto state = validateName(s); + input = QString::fromStdString(s); + return toQtState(state); +} + +QValidator::State NodeValidator::validate(QString &input, int &pos) const { + return toQtState(validateNodes(input.toStdString())); +} + +QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { + return toQtState(validateNonWhitespace(input.toStdString())); +} + +QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { + return toQtState(validateIpAddress(input.toStdString())); +} + +QValidator::State DoubleValidator::validate(QString &input, int &pos) const { + return toQtState(validateDouble(input.toLatin1().toStdString())); +} + +namespace utils { + +bool isDarkTheme() { + QColor windowColor = QApplication::palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + +QPixmap icon(const QString &id) { + bool dark_theme = isDarkTheme(); + + QPixmap pm; + QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); + if (!QPixmapCache::find(key, &pm)) { + pm = bootstrapPixmap(id); + if (dark_theme) { + QPainter p(&pm); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(pm.rect(), QColor("#bbbbbb")); + } + QPixmapCache::insert(key, pm); + } + return pm; +} + +void setTheme(int theme) { + auto style = QApplication::style(); + if (!style) return; + + static int prev_theme = 0; + if (theme != prev_theme) { + prev_theme = theme; + QPalette new_palette; + if (theme == DARK_THEME) { + new_palette.setColor(QPalette::Window, toQColor(DarkTheme::window)); + new_palette.setColor(QPalette::WindowText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Base, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::AlternateBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipText, toQColor(DarkTheme::tooltip_text)); + new_palette.setColor(QPalette::Text, toQColor(DarkTheme::text)); + new_palette.setColor(QPalette::Button, toQColor(DarkTheme::button)); + new_palette.setColor(QPalette::ButtonText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Highlight, toQColor(DarkTheme::highlight)); + new_palette.setColor(QPalette::HighlightedText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::BrightText, toQColor(DarkTheme::bright_text)); + new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::WindowText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::Text, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Light, toQColor(DarkTheme::light)); + new_palette.setColor(QPalette::Dark, toQColor(DarkTheme::dark)); + } else { + new_palette = style->standardPalette(); + } + qApp->setPalette(new_palette); + style->polish(qApp); + for (auto w : QApplication::allWidgets()) { + w->setPalette(new_palette); + } + } +} + +} // namespace utils + +void sigTermHandler(int s) { + std::signal(s, SIG_DFL); + qApp->quit(); +} + +void initApp(int argc, char *argv[], bool disable_hidpi) { + // setup signal handlers to exit gracefully + std::signal(SIGINT, sigTermHandler); + std::signal(SIGTERM, sigTermHandler); + +#ifdef __APPLE__ + // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering + QApplication tmp(argc, argv); + if (disable_hidpi) { + qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); + } +#endif + + qputenv("QT_DBL_CLICK_DIST", "150"); + // ensure the current dir matches the exectuable's directory + std::error_code ec; + std::filesystem::current_path(executableDir(), ec); +} + +QPixmap bootstrapPixmap(const QString &id) { + QPixmap pixmap; + const std::string svg = utils::bootstrapSvg(id.toStdString()); + if (!svg.empty()) { + pixmap.loadFromData((const uchar *)svg.data(), svg.size(), "svg"); + } + return pixmap; +} diff --git a/openpilot/tools/cabana/utils/qtutil.h b/openpilot/tools/cabana/utils/qtutil.h new file mode 100644 index 0000000000..db0417ec9a --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/core/observable.h" +#include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/settings.h" +#include "tools/cabana/utils/strings.h" +#include "tools/cabana/utils/util.h" + +// needed by QVariant::fromValue() in the Qt views; goes away with QVariant +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + +inline QColor toQColor(const CabanaColor &color) { + return QColor(color.r, color.g, color.b, color.a); +} + +class LogSlider : public QSlider { + Q_OBJECT + +public: + LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : scale(factor), QSlider(orientation, parent) {} + + void setRange(double min, double max) { + scale.setRange(min, max); + QSlider::setRange(min, max); + setValue(QSlider::value()); + } + int value() const { return scale.value(QSlider::value(), minimum(), maximum()); } + void setValue(int v) { QSlider::setValue(scale.position(v, minimum(), maximum())); } + +private: + LogScale scale; +}; + +enum { + ColorsRole = Qt::UserRole + 1, + BytesRole = Qt::UserRole + 2 +}; + +class MessageBytesDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + 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; + bool multipleLines() const { return multiple_lines; } + void setMultipleLines(bool v) { multiple_lines = v; } + QSize sizeForBytes(int n) const; + +private: + std::array hex_text_table; + QFontMetrics font_metrics; + QFont fixed_font; + QSize byte_size = {}; + bool multiple_lines = false; + int h_margin, v_margin; +}; + +// QValidator wrappers around the std::string validators in util.h +#define CABANA_VALIDATOR(Name) \ + class Name : public QValidator { \ + Q_OBJECT \ + public: \ + Name(QObject *parent = nullptr) : QValidator(parent) {} \ + QValidator::State validate(QString &input, int &pos) const override; \ + }; +CABANA_VALIDATOR(NameValidator) +CABANA_VALIDATOR(NodeValidator) +CABANA_VALIDATOR(NonWhitespaceValidator) +CABANA_VALIDATOR(IpAddressValidator) +CABANA_VALIDATOR(DoubleValidator) +#undef CABANA_VALIDATOR + +namespace utils { + +QPixmap icon(const QString &id); +bool isDarkTheme(); +void setTheme(int theme); +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); +} +inline auto qbytes(const std::vector &dat) { + return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); +} + +} + +class ToolButton : public QToolButton { + Q_OBJECT +public: + ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { + setIcon(icon); + setToolTip(tooltip); + setAutoRaise(true); + const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + setIconSize({metric, metric}); + theme = settings.theme; + settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); + } + void setIcon(const QString &icon) { + icon_str = icon; + QToolButton::setIcon(utils::icon(icon_str)); + } + +private: + void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + Connection settings_connection_; + QString icon_str; + int theme; +}; + +class TabBar : public QTabBar { + Q_OBJECT + +public: + TabBar(QWidget *parent) : QTabBar(parent) {} + int addTab(const QString &text); + +private: + void closeTabClicked(); +}; + +void initApp(int argc, char *argv[], bool disable_hidpi = true); +QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index d5cd77a8c8..7bdf225975 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -1,18 +1,19 @@ #include "tools/cabana/utils/util.h" #include +#include #include -#include #include #include #include #include +#include #include #include -#include #include #include #include +#include #include #include #include @@ -20,11 +21,6 @@ #include #endif -#include -#include -#include -#include -#include #include "common/util.h" static const std::thread::id main_thread_id = std::this_thread::get_id(); @@ -53,22 +49,21 @@ void utils::drainMainThreadQueue() { // SegmentTree -void SegmentTree::build(const std::vector &arr) { - size = arr.size(); +void SegmentTree::build(int n, const std::function &y) { + size = n; tree.resize(4 * size); // size of the tree is 4 times the size of the array if (size > 0) { - build_tree(arr, 1, 0, size - 1); + build_tree(y, 1, 0, size - 1); } } -void SegmentTree::build_tree(const std::vector &arr, int n, int left, int right) { +void SegmentTree::build_tree(const std::function &y, int n, int left, int right) { if (left == right) { - const double y = arr[left].y(); - tree[n] = {y, y}; + tree[n] = {y(left), y(left)}; } else { const int mid = (left + right) >> 1; - build_tree(arr, 2 * n, left, mid); - build_tree(arr, 2 * n + 1, mid + 1, right); + build_tree(y, 2 * n, left, mid); + build_tree(y, 2 * n + 1, mid + 1, right); tree[n] = {std::min(tree[2 * n].first, tree[2 * n + 1].first), std::max(tree[2 * n].second, tree[2 * n + 1].second)}; } } @@ -84,119 +79,22 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in return {std::min(l.first, r.first), std::max(l.second, r.second)}; } -// MessageBytesDelegate - -MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) - : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { - fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); - for (int i = 0; i < 256; ++i) { - hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); - hex_text_table[i].prepare({}, fixed_font); - } - h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; -} - -QSize MessageBytesDelegate::sizeForBytes(int n) const { - int rows = multiple_lines ? std::max(1, n / 8) : 1; - return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; -} - -QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto data = index.data(BytesRole); - return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); -} - -void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); - } - - QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); - QColor highlighted_color = option.palette.color(QPalette::HighlightedText); - auto text_color = index.data(Qt::ForegroundRole).value(); - bool inactive = text_color.isValid(); - if (!inactive) { - text_color = option.palette.color(QPalette::Text); - } - auto data = index.data(BytesRole); - if (!data.isValid()) { - painter->setFont(option.font); - painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); - painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); - return; - } - - // Paint hex column - const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); - - painter->setFont(fixed_font); - const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - const QPoint pt = item_rect.topLeft(); - for (int i = 0; i < bytes.size(); ++i) { - int row = !multiple_lines ? 0 : i / 8; - int column = !multiple_lines ? i : i % 8; - QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); - - 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, toQColor(colors[i])); - } else { - painter->setPen(text_pen); - } - utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); - } -} - -// TabBar - -int TabBar::addTab(const QString &text) { - int index = QTabBar::addTab(text); - QToolButton *btn = new ToolButton("x", tr("Close Tab")); - int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); - int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); - btn->setFixedSize({width, height}); - setTabButton(index, QTabBar::RightSide, btn); - QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); - return index; -} - -void TabBar::closeTabClicked() { - QObject *object = sender(); - for (int i = 0; i < count(); ++i) { - if (tabButton(i, QTabBar::RightSide) == object) { - emit tabCloseRequested(i); - break; - } - } -} - // UnixSignalHandler -UnixSignalHandler::UnixSignalHandler() { +UnixSignalHandler::UnixSignalHandler(std::function on_signal) { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { - qFatal("Couldn't create TERM socketpair"); + fprintf(stderr, "Couldn't create TERM socketpair\n"); + abort(); } - waiter = std::thread([this]() { + waiter = std::thread([this, on_signal = std::move(on_signal)]() { int tmp = 0; while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { if (errno != EINTR) return; } if (shutting_down.load()) return; - // Marshal exit onto the GUI thread (qApp methods are not thread-safe). - QMetaObject::invokeMethod(qApp, []() { - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); - }, Qt::QueuedConnection); + on_signal(); }); std::signal(SIGINT, signalHandler); @@ -216,118 +114,131 @@ void UnixSignalHandler::signalHandler(int s) { (void)!::write(sig_fd[0], &s, sizeof(s)); } -// NameValidator +// validators -NameValidator::NameValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NameValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - input.replace(' ', '_'); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid; +ValidState validateName(std::string &input) { + std::replace(input.begin(), input.end(), ' ', '_'); + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (!std::isalnum(c) && c != '_') return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// NodeValidator - -NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NodeValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateNodes(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; // Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing). bool need_word = true; - for (const QChar &c : input) { - if (c.isLetterOrNumber() || c == '_') { + for (const unsigned char c : input) { + if (std::isalnum(c) || c == '_') { need_word = false; } else if (c == ',' && !need_word) { need_word = true; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return need_word ? QValidator::Intermediate : QValidator::Acceptable; + return need_word ? ValidState::Intermediate : ValidState::Acceptable; } -// NonWhitespaceValidator - -NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (c.isSpace()) return QValidator::Invalid; +ValidState validateNonWhitespace(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (std::isspace(c)) return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// IpAddressValidator - -IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateIpAddress(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; int dots = 0; int value = 0; bool has_digit = false; - for (const QChar &c : input) { - if (c.isDigit()) { - value = has_digit ? value * 10 + c.digitValue() : c.digitValue(); - if (value > 255) return QValidator::Invalid; + for (const unsigned char c : input) { + if (std::isdigit(c)) { + value = has_digit ? value * 10 + (c - '0') : (c - '0'); + if (value > 255) return ValidState::Invalid; has_digit = true; } else if (c == '.') { - if (!has_digit || dots >= 3) return QValidator::Invalid; + if (!has_digit || dots >= 3) return ValidState::Invalid; ++dots; has_digit = false; value = 0; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate; + return (dots == 3 && has_digit) ? ValidState::Acceptable : ValidState::Intermediate; } -DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {} +ValidState validateDouble(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; -QValidator::State DoubleValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - - // Match QString::toDouble(): C locale, no hex floats / inf / nan. - const std::string bytes = input.toLatin1().toStdString(); - // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. - if (bytes.find_first_of("xXpP") != std::string::npos) { - return QValidator::Invalid; + // C locale, no hex floats / p-exponents / inf / nan (strtod accepts them, the DBC parser does not) + if (input.find_first_of("xXpP") != std::string::npos) { + return ValidState::Invalid; } - const char *start = bytes.c_str(); + const char *start = input.c_str(); char *end = nullptr; const double value = std::strtod(start, &end); if (end == start) { // Still typing a sign, decimal point, or exponent prefix. if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") { - return QValidator::Intermediate; + return ValidState::Intermediate; } - return QValidator::Invalid; + return ValidState::Invalid; } if (*end == '\0') { - // Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not). - return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid; + return std::isfinite(value) ? ValidState::Acceptable : ValidState::Invalid; } // Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1."). for (const char *p = end; *p; ++p) { const char c = *p; if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) { - return QValidator::Invalid; + return ValidState::Invalid; } } - return QValidator::Intermediate; + return ValidState::Intermediate; +} + +// embedded at build time from the bootstrap_icons package (see SConscript) +extern const unsigned char bootstrap_icons_svg[]; +extern const size_t bootstrap_icons_svg_len; + +static std::unordered_map load_bootstrap_icons() { + std::unordered_map icons; + + const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); + const std::string sym_open = " with + svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) + icons[id] = std::move(svg_str); + } + } + pos = end; + } + return icons; } namespace utils { @@ -384,77 +295,19 @@ bool setClipboardText(const std::string &text) { return false; } -bool isDarkTheme() { - QColor windowColor = QApplication::palette().color(QPalette::Window); - return windowColor.lightness() < 128; -} - -QPixmap icon(const QString &id) { - bool dark_theme = isDarkTheme(); - - QPixmap pm; - QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); - if (!QPixmapCache::find(key, &pm)) { - pm = bootstrapPixmap(id); - if (dark_theme) { - QPainter p(&pm); - p.setCompositionMode(QPainter::CompositionMode_SourceIn); - p.fillRect(pm.rect(), QColor("#bbbbbb")); - } - QPixmapCache::insert(key, pm); - } - return pm; -} - -void setTheme(int theme) { - auto style = QApplication::style(); - if (!style) return; - - static int prev_theme = 0; - if (theme != prev_theme) { - prev_theme = theme; - QPalette new_palette; - if (theme == DARK_THEME) { - // "Darcula" like dark theme - new_palette.setColor(QPalette::Window, QColor("#353535")); - new_palette.setColor(QPalette::WindowText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Base, QColor("#3c3f41")); - new_palette.setColor(QPalette::AlternateBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipText, QColor("#bbb")); - new_palette.setColor(QPalette::Text, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Button, QColor("#3c3f41")); - new_palette.setColor(QPalette::ButtonText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Highlight, QColor("#2f65ca")); - new_palette.setColor(QPalette::HighlightedText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::BrightText, QColor("#f0f0f0")); - new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::Text, QColor("#777777")); - new_palette.setColor(QPalette::Light, QColor("#777777")); - new_palette.setColor(QPalette::Dark, QColor("#353535")); - } else { - new_palette = style->standardPalette(); - } - qApp->setPalette(new_palette); - style->polish(qApp); - for (auto w : QApplication::allWidgets()) { - w->setPalette(new_palette); - } - } +std::string bootstrapSvg(const std::string &id) { + static auto icons = load_bootstrap_icons(); + auto it = icons.find(id); + return it != icons.end() ? it->second : std::string(); } } // namespace utils int num_decimals(double num) { - const QString string = QString::number(num); - auto dot_pos = string.indexOf('.'); - return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; -} - -void sigTermHandler(int s) { - std::signal(s, SIG_DFL); - qApp->quit(); + char buf[32]; + snprintf(buf, sizeof(buf), "%g", num); + const char *dot = strpbrk(buf, ".,"); // Qt sets LC_ALL from the environment so the decimal mark may be a comma + return dot ? (int)strlen(dot + 1) : 0; } std::filesystem::path executableDir() { @@ -469,70 +322,3 @@ std::filesystem::path executableDir() { return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif } - -void initApp(int argc, char *argv[], bool disable_hidpi) { - // setup signal handlers to exit gracefully - std::signal(SIGINT, sigTermHandler); - std::signal(SIGTERM, sigTermHandler); - -#ifdef __APPLE__ - // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering - QApplication tmp(argc, argv); - if (disable_hidpi) { - qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); - } -#endif - - qputenv("QT_DBL_CLICK_DIST", "150"); - // ensure the current dir matches the exectuable's directory - std::error_code ec; - std::filesystem::current_path(executableDir(), ec); -} - -// embedded at build time from the bootstrap_icons package (see SConscript) -extern const unsigned char bootstrap_icons_svg[]; -extern const size_t bootstrap_icons_svg_len; - -static std::unordered_map load_bootstrap_icons() { - std::unordered_map icons; - - const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); - const std::string sym_open = " with - svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) - icons[id] = std::move(svg_str); - } - } - pos = end; - } - return icons; -} - -QPixmap bootstrapPixmap(const QString &id) { - static auto icons = load_bootstrap_icons(); - - QPixmap pixmap; - auto it = icons.find(id.toStdString()); - if (it != icons.end()) { - pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg"); - } - return pixmap; -} diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index e0d13208a6..324c966b5a 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -10,136 +10,68 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/core/observable.h" -#include "tools/cabana/dbc/dbc.h" -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/strings.h" - -// needed by QVariant::fromValue() in the Qt views; goes away with QVariant -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -inline QColor toQColor(const CabanaColor &color) { - return QColor(color.r, color.g, color.b, color.a); -} - -class LogSlider : public QSlider { - Q_OBJECT +#include "tools/cabana/core/color.h" +class SegmentTree { public: - LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {} + SegmentTree() = default; + void build(int n, const std::function &y); + inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +private: + std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; + void build_tree(const std::function &y, int n, int left, int right); + std::vector> tree; + int size = 0; +}; + +// maps a linear slider position onto a log10 scale +class LogScale { +public: + LogScale(double factor) : factor(factor) {} void setRange(double min, double max) { log_min = factor * std::log10(min); log_max = factor * std::log10(max); - QSlider::setRange(min, max); - setValue(QSlider::value()); } - int value() const { - double v = log_min + (log_max - log_min) * ((QSlider::value() - minimum()) / double(maximum() - minimum())); + int value(int pos, int pos_min, int pos_max) const { + double v = log_min + (log_max - log_min) * ((pos - pos_min) / double(pos_max - pos_min)); return std::lround(std::pow(10, v / factor)); } - void setValue(int v) { + int position(int v, int pos_min, int pos_max) const { double log_v = std::clamp(factor * std::log10(v), log_min, log_max); - v = minimum() + (maximum() - minimum()) * ((log_v - log_min) / (log_max - log_min)); - QSlider::setValue(v); + return pos_min + (pos_max - pos_min) * ((log_v - log_min) / (log_max - log_min)); } private: double factor, log_min = 0, log_max = 1; }; -enum { - ColorsRole = Qt::UserRole + 1, - BytesRole = Qt::UserRole + 2 -}; +enum class ValidState { Invalid, Intermediate, Acceptable }; -class SegmentTree { -public: - SegmentTree() = default; - void build(const std::vector &arr); - inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +// single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_' +ValidState validateName(std::string &input); +// comma-separated identifiers: \w+(,\w+)* +ValidState validateNodes(const std::string &input); +// one or more non-whitespace characters (\S+) +ValidState validateNonWhitespace(const std::string &input); +// dotted IPv4 address (0-255 per octet) +ValidState validateIpAddress(const std::string &input); +// C-locale floating-point +ValidState validateDouble(const std::string &input); -private: - std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; - void build_tree(const std::vector &arr, int n, int left, int right); - std::vector> tree; - int size = 0; -}; - -class MessageBytesDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - 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; - bool multipleLines() const { return multiple_lines; } - void setMultipleLines(bool v) { multiple_lines = v; } - QSize sizeForBytes(int n) const; - -private: - std::array hex_text_table; - QFontMetrics font_metrics; - QFont fixed_font; - QSize byte_size = {}; - bool multiple_lines = false; - int h_margin, v_margin; -}; - -// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'. -class NameValidator : public QValidator { - Q_OBJECT -public: - NameValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts comma-separated identifiers: \w+(,\w+)* -class NodeValidator : public QValidator { - Q_OBJECT -public: - NodeValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts one or more non-whitespace characters (\S+). -class NonWhitespaceValidator : public QValidator { - Q_OBJECT -public: - NonWhitespaceValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts a dotted IPv4 address (0-255 per octet). -class IpAddressValidator : public QValidator { - Q_OBJECT -public: - IpAddressValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// C-locale floating-point validator (matches QString::toDouble). -class DoubleValidator : public QValidator { - Q_OBJECT -public: - DoubleValidator(QObject *parent = nullptr); - QValidator::State validate(QString &input, int &pos) const override; +// "Darcula" like dark theme +struct DarkTheme { + static constexpr CabanaColor window{0x35, 0x35, 0x35}; + static constexpr CabanaColor window_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor base{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor tooltip_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor button{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor highlight{0x2f, 0x65, 0xca}; + static constexpr CabanaColor bright_text{0xf0, 0xf0, 0xf0}; + static constexpr CabanaColor disabled_text{0x77, 0x77, 0x77}; + static constexpr CabanaColor light{0x77, 0x77, 0x77}; + static constexpr CabanaColor dark{0x35, 0x35, 0x35}; }; namespace utils { @@ -148,67 +80,23 @@ bool isMainThread(); // inline on the main thread, queued until drainMainThreadQueue() otherwise void runOnMainThread(std::function fn); void drainMainThreadQueue(); -QPixmap icon(const QString &id); std::string homePath(); std::filesystem::path configPath(); bool getClipboardText(std::string *text); // false if no clipboard tool is available bool setClipboardText(const std::string &text); -bool isDarkTheme(); -void setTheme(int theme); -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); -} +std::string bootstrapSvg(const std::string &id); // empty if unknown // boundary conversions for the remaining Qt byte-array based state APIs template std::vector toBytes(const T &dat) { return {dat.begin(), dat.end()}; } -inline auto qbytes(const std::vector &dat) { - return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); -} } -class ToolButton : public QToolButton { - Q_OBJECT -public: - ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { - setIcon(icon); - setToolTip(tooltip); - setAutoRaise(true); - const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); - setIconSize({metric, metric}); - theme = settings.theme; - settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); - } - void setIcon(const QString &icon) { - icon_str = icon; - QToolButton::setIcon(utils::icon(icon_str)); - } - -private: - void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } - Connection settings_connection_; - QString icon_str; - int theme; -}; - -class TabBar : public QTabBar { - Q_OBJECT - -public: - TabBar(QWidget *parent) : QTabBar(parent) {} - int addTab(const QString &text); - -private: - void closeTabClicked(); -}; - -// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread -// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread. +// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread. +// on_signal runs on the waiter thread; the caller marshals to the GUI thread. class UnixSignalHandler { public: - UnixSignalHandler(); + UnixSignalHandler(std::function on_signal); ~UnixSignalHandler(); static void signalHandler(int s); @@ -220,5 +108,3 @@ private: int num_decimals(double num); std::filesystem::path executableDir(); -void initApp(int argc, char *argv[], bool disable_hidpi = true); -QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 23f60f29f1..f7b1b41628 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -12,6 +12,7 @@ #include #include "tools/cabana/tools/routeinfo.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index 1b6d81406e..8416eece59 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -12,7 +12,7 @@ #include #include "tools/cabana/cameraview.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h"