cabana: split utils/util into Qt-free util and qtutil (#38723)

This commit is contained in:
Trey Moen
2026-08-28 11:37:09 -07:00
committed by GitHub
parent 633d17cd12
commit 5645370f84
30 changed files with 566 additions and 482 deletions
+3 -1
View File
@@ -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])
+1
View File
@@ -13,6 +13,7 @@
#include <QToolTip>
#include "tools/cabana/commands.h"
#include "tools/cabana/utils/qtutil.h"
// BinaryView
+1
View File
@@ -4,6 +4,7 @@
#include <tuple>
#include <vector>
#include <QStaticText>
#include <QStyledItemDelegate>
#include <QTableView>
+9 -1
View File
@@ -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;
+1 -1
View File
@@ -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(); });
}
}
}
+1
View File
@@ -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,
@@ -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;
@@ -8,6 +8,7 @@
#include <QVBoxLayout>
#include "tools/cabana/streams/abstractstream.h"
#include "tools/cabana/utils/qtutil.h"
SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) {
setWindowTitle(title);
@@ -3,6 +3,7 @@
#include <algorithm>
#include <limits>
#include <QPainter>
#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()) {
+1 -1
View File
@@ -7,7 +7,7 @@
#include <QToolTip>
#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);
+1
View File
@@ -1,5 +1,6 @@
#include "tools/cabana/commands.h"
#include <cassert>
#include <cmath>
// UndoStack
+2
View File
@@ -1,6 +1,7 @@
#pragma once
#include <QDialogButtonBox>
#include <QSpinBox>
#include <QSplitter>
#include <QTabWidget>
#include <QTextEdit>
@@ -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:
+1
View File
@@ -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:
+1
View File
@@ -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"
+2
View File
@@ -11,11 +11,13 @@
#include <QHeaderView>
#include <QLineEdit>
#include <QMenu>
#include <QPushButton>
#include <QTreeView>
#include <QWheelEvent>
#include "tools/cabana/dbc/dbcmanager.h"
#include "tools/cabana/streams/abstractstream.h"
#include "tools/cabana/utils/qtutil.h"
class MessageListModel : public QAbstractTableModel {
Q_OBJECT
+1 -1
View File
@@ -9,7 +9,7 @@
#include <QVBoxLayout>
#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;
+2 -1
View File
@@ -11,10 +11,11 @@
#include <QPainterPath>
#include <QPushButton>
#include <QScrollBar>
#include <QSpinBox>
#include <QVBoxLayout>
#include "tools/cabana/commands.h"
#include "tools/cabana/utils/util.h"
#include "tools/cabana/utils/qtutil.h"
// SignalModel
@@ -1,5 +1,6 @@
#include "tools/cabana/streams/abstractstream.h"
#include <cassert>
#include <limits>
#include <utility>
@@ -1,5 +1,6 @@
#include "tools/cabana/streams/devicestream.h"
#include <cassert>
#include <cerrno>
#include <chrono>
#include <csignal>
@@ -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) {}
@@ -4,6 +4,7 @@
#include "common/timing.h"
#include "common/util.h"
#include "tools/cabana/settings.h"
ReplayStream::ReplayStream() {
unsetenv("ZMQ");
+1
View File
@@ -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
@@ -10,6 +10,8 @@
#include <QTimer>
#include <QVBoxLayout>
#include "tools/cabana/utils/qtutil.h"
// FindSignalModel
QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const {
@@ -7,8 +7,13 @@
#include <QAbstractTableModel>
#include <QCheckBox>
#include <QComboBox>
#include <QDialog>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSpinBox>
#include <QTableView>
#include "tools/cabana/commands.h"
+236
View File
@@ -0,0 +1,236 @@
#include "tools/cabana/utils/qtutil.h"
#include <algorithm>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <string>
#include <QColor>
#include <QFontDatabase>
#include <QPixmapCache>
#include <QPainterPath>
// 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<std::vector<uint8_t> *>(data.value<void *>())->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<QColor>();
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<std::vector<uint8_t> *>(data.value<void *>());
const auto &colors = *static_cast<std::vector<CabanaColor> *>(index.data(ColorsRole).value<void *>());
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;
}
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <array>
#include <utility>
#include <QApplication>
#include <QColor>
#include <QFont>
#include <QFontMetrics>
#include <QPainter>
#include <QStaticText>
#include <QStringBuilder>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QValidator>
#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<QStaticText, 256> 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<uint8_t> &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);
+98 -312
View File
@@ -1,18 +1,19 @@
#include "tools/cabana/utils/util.h"
#include <algorithm>
#include <cctype>
#include <cerrno>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <csignal>
#include <cstring>
#include <filesystem>
#include <limits>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <unordered_map>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -20,11 +21,6 @@
#include <mach-o/dyld.h>
#endif
#include <QColor>
#include <QFontDatabase>
#include <QPixmapCache>
#include <QPainterPath>
#include <unordered_map>
#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<QPointF> &arr) {
size = arr.size();
void SegmentTree::build(int n, const std::function<double(int)> &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<QPointF> &arr, int n, int left, int right) {
void SegmentTree::build_tree(const std::function<double(int)> &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<double, double> 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<std::vector<uint8_t> *>(data.value<void *>())->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<QColor>();
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<std::vector<uint8_t> *>(data.value<void *>());
const auto &colors = *static_cast<std::vector<CabanaColor> *>(index.data(ColorsRole).value<void *>());
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<void()> 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<std::string, std::string> load_bootstrap_icons() {
std::unordered_map<std::string, std::string> icons;
const std::string content(reinterpret_cast<const char *>(bootstrap_icons_svg), bootstrap_icons_svg_len);
const std::string sym_open = "<symbol ";
const std::string sym_close = "</symbol>";
const std::string id_attr = "id=\"";
size_t pos = 0;
while ((pos = content.find(sym_open, pos)) != std::string::npos) {
size_t end = content.find(sym_close, pos);
if (end == std::string::npos) break;
end += sym_close.size();
// extract id
size_t id_start = content.find(id_attr, pos);
if (id_start != std::string::npos && id_start < end) {
id_start += id_attr.size();
size_t id_end = content.find('"', id_start);
if (id_end != std::string::npos && id_end < end) {
std::string id = content.substr(id_start, id_end - id_start);
std::string svg_str = content.substr(pos, end - pos);
// replace <symbol with <svg, </symbol> with </svg>
svg_str.replace(0, 7, "<svg"); // "<symbol" (7) -> "<svg" (4)
svg_str.replace(svg_str.size() - 9, 9, "</svg>"); // "</symbol>" (9) -> "</svg>" (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<std::string, std::string> load_bootstrap_icons() {
std::unordered_map<std::string, std::string> icons;
const std::string content(reinterpret_cast<const char *>(bootstrap_icons_svg), bootstrap_icons_svg_len);
const std::string sym_open = "<symbol ";
const std::string sym_close = "</symbol>";
const std::string id_attr = "id=\"";
size_t pos = 0;
while ((pos = content.find(sym_open, pos)) != std::string::npos) {
size_t end = content.find(sym_close, pos);
if (end == std::string::npos) break;
end += sym_close.size();
// extract id
size_t id_start = content.find(id_attr, pos);
if (id_start != std::string::npos && id_start < end) {
id_start += id_attr.size();
size_t id_end = content.find('"', id_start);
if (id_end != std::string::npos && id_end < end) {
std::string id = content.substr(id_start, id_end - id_start);
std::string svg_str = content.substr(pos, end - pos);
// replace <symbol with <svg, </symbol> with </svg>
svg_str.replace(0, 7, "<svg"); // "<symbol" (7) -> "<svg" (4)
svg_str.replace(svg_str.size() - 9, 9, "</svg>"); // "</symbol>" (9) -> "</svg>" (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;
}
+49 -163
View File
@@ -1,6 +1,6 @@
#pragma once
#include <array>
#include <algorithm>
#include <atomic>
#include <cmath>
#include <filesystem>
@@ -10,136 +10,68 @@
#include <vector>
#include <utility>
#include <QApplication>
#include <QColor>
#include <QComboBox>
#include <QDialog>
#include <QFont>
#include <QFontMetrics>
#include <QGroupBox>
#include <QLineEdit>
#include <QPainter>
#include <QSpinBox>
#include <QStaticText>
#include <QStringBuilder>
#include <QStyledItemDelegate>
#include <QToolButton>
#include <QValidator>
#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<double(int)> &y);
inline std::pair<double, double> minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); }
private:
std::pair<double, double> get_minmax(int n, int left, int right, int range_left, int range_right) const;
void build_tree(const std::function<double(int)> &y, int n, int left, int right);
std::vector<std::pair<double, double>> 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<QPointF> &arr);
inline std::pair<double, double> 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<double, double> get_minmax(int n, int left, int right, int range_left, int range_right) const;
void build_tree(const std::vector<QPointF> &arr, int n, int left, int right);
std::vector<std::pair<double, double>> 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<QStaticText, 256> 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<void()> 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 <typename T>
std::vector<uint8_t> toBytes(const T &dat) { return {dat.begin(), dat.end()}; }
inline auto qbytes(const std::vector<uint8_t> &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<void()> 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);
+1
View File
@@ -12,6 +12,7 @@
#include <QVBoxLayout>
#include "tools/cabana/tools/routeinfo.h"
#include "tools/cabana/utils/qtutil.h"
const int MIN_VIDEO_HEIGHT = 100;
const int THUMBNAIL_MARGIN = 3;
+1 -1
View File
@@ -12,7 +12,7 @@
#include <QTabBar>
#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"