IQ.Pilot Release Commit @ 0b96bd5
This commit is contained in:
@@ -1,29 +0,0 @@
|
||||
#include "tools/cabana/utils/elidedlabel.h"
|
||||
#include <QPainter>
|
||||
#include <QStyleOption>
|
||||
|
||||
ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {}
|
||||
|
||||
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) {
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
|
||||
setMinimumWidth(1);
|
||||
}
|
||||
|
||||
void ElidedLabel::resizeEvent(QResizeEvent* event) {
|
||||
QLabel::resizeEvent(event);
|
||||
lastText_ = elidedText_ = "";
|
||||
}
|
||||
|
||||
void ElidedLabel::paintEvent(QPaintEvent *event) {
|
||||
const QString curText = text();
|
||||
if (curText != lastText_) {
|
||||
elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width());
|
||||
lastText_ = curText;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
drawFrame(&painter);
|
||||
QStyleOption opt;
|
||||
opt.initFrom(this);
|
||||
style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole());
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
|
||||
class ElidedLabel : public QLabel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ElidedLabel(QWidget *parent = 0);
|
||||
explicit ElidedLabel(const QString &text, QWidget *parent = 0);
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override {
|
||||
if (rect().contains(event->pos())) {
|
||||
emit clicked();
|
||||
}
|
||||
}
|
||||
QString lastText_, elidedText_;
|
||||
};
|
||||
@@ -42,4 +42,4 @@ void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@
|
||||
namespace utils {
|
||||
void exportToCSV(const std::string &file_name, std::optional<MessageId> msg_id = std::nullopt);
|
||||
void exportSignalsToCSV(const std::string &file_name, const MessageId &msg_id);
|
||||
} // namespace utils
|
||||
}
|
||||
|
||||
57
iqpilot/tools/cabana/utils/strings.cc
Normal file
57
iqpilot/tools/cabana/utils/strings.cc
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "tools/cabana/utils/strings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
|
||||
#include "tools/cabana/dbc/dbc.h"
|
||||
|
||||
namespace utils {
|
||||
|
||||
std::string formatSeconds(double sec, bool include_milliseconds, bool absolute_time) {
|
||||
char out[80] = {};
|
||||
if (absolute_time) {
|
||||
const auto ms_total = static_cast<int64_t>(std::llround(sec * 1000.0));
|
||||
const std::time_t secs = static_cast<std::time_t>(ms_total / 1000);
|
||||
int millis = static_cast<int>(ms_total % 1000);
|
||||
if (millis < 0) millis = -millis;
|
||||
std::tm tm{};
|
||||
localtime_r(&secs, &tm);
|
||||
char buf[64] = {};
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
if (!include_milliseconds) return buf;
|
||||
snprintf(out, sizeof(out), "%s.%03d", buf, millis);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
const bool show_hours = sec > 60 * 60;
|
||||
int total_ms = static_cast<int>(std::llround(std::max(0.0, sec) * 1000.0));
|
||||
const int hours = total_ms / (3600 * 1000);
|
||||
const int minutes = (total_ms / (60 * 1000)) % 60;
|
||||
const int seconds = (total_ms / 1000) % 60;
|
||||
const int millis = total_ms % 1000;
|
||||
if (show_hours && include_milliseconds) {
|
||||
snprintf(out, sizeof(out), "%02d:%02d:%02d.%03d", hours, minutes, seconds, millis);
|
||||
} else if (show_hours) {
|
||||
snprintf(out, sizeof(out), "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
} else if (include_milliseconds) {
|
||||
snprintf(out, sizeof(out), "%02d:%02d.%03d", minutes, seconds, millis);
|
||||
} else {
|
||||
snprintf(out, sizeof(out), "%02d:%02d", minutes, seconds);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string signalToolTip(const cabana::Signal *sig) {
|
||||
std::ostringstream s;
|
||||
s << "\n " << sig->name << "<br /><span font-size:small\">\n"
|
||||
<< " Start Bit: " << sig->start_bit << " Size: " << sig->size << "<br />\n"
|
||||
<< " MSB: " << sig->msb << " LSB: " << sig->lsb << "<br />\n"
|
||||
<< " Little Endian: " << (sig->is_little_endian ? "Y" : "N")
|
||||
<< " Signed: " << (sig->is_signed ? "Y" : "N") << "</span>\n ";
|
||||
return s.str();
|
||||
}
|
||||
|
||||
}
|
||||
109
iqpilot/tools/cabana/utils/strings.h
Normal file
109
iqpilot/tools/cabana/utils/strings.h
Normal file
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace cabana { class Signal; }
|
||||
|
||||
namespace utils {
|
||||
|
||||
std::string formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
|
||||
std::string signalToolTip(const cabana::Signal *sig);
|
||||
|
||||
inline std::string trimmed(const std::string &s) {
|
||||
const char *ws = " \t\n\r\f\v";
|
||||
size_t b = s.find_first_not_of(ws);
|
||||
if (b == std::string::npos) return "";
|
||||
return s.substr(b, s.find_last_not_of(ws) - b + 1);
|
||||
}
|
||||
|
||||
inline bool containsCI(const std::string &s, const std::string &txt) {
|
||||
auto it = std::search(s.begin(), s.end(), txt.begin(), txt.end(),
|
||||
[](unsigned char a, unsigned char b) { return std::tolower(a) == std::tolower(b); });
|
||||
return it != s.end();
|
||||
}
|
||||
|
||||
|
||||
inline std::string stripHtml(const std::string &s) {
|
||||
std::string out;
|
||||
bool in_tag = false;
|
||||
for (char c : s) {
|
||||
if (c == '<') in_tag = true;
|
||||
else if (c == '>') in_tag = false;
|
||||
else if (!in_tag) out += c;
|
||||
}
|
||||
return trimmed(out);
|
||||
}
|
||||
|
||||
inline std::vector<std::string> split(const std::string &s, char sep) {
|
||||
std::vector<std::string> parts;
|
||||
size_t start = 0;
|
||||
for (size_t pos; (pos = s.find(sep, start)) != std::string::npos; start = pos + 1) {
|
||||
parts.push_back(s.substr(start, pos - start));
|
||||
}
|
||||
parts.push_back(s.substr(start));
|
||||
return parts;
|
||||
}
|
||||
|
||||
inline std::string toString(double v) {
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%g", v);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
inline double toDouble(const std::string &s) {
|
||||
char *end = nullptr;
|
||||
double v = std::strtod(s.c_str(), &end);
|
||||
return (end != s.c_str() && *end == '\0') ? v : 0.0;
|
||||
}
|
||||
|
||||
|
||||
inline int toInt(const std::string &s) {
|
||||
char *end = nullptr;
|
||||
long v = std::strtol(s.c_str(), &end, 10);
|
||||
return (end != s.c_str() && *end == '\0') ? (int)v : 0;
|
||||
}
|
||||
|
||||
|
||||
inline unsigned long toULong(const std::string &s, int base = 10) {
|
||||
char *end = nullptr;
|
||||
unsigned long v = std::strtoul(s.c_str(), &end, base);
|
||||
return (end != s.c_str() && *end == '\0') ? v : 0;
|
||||
}
|
||||
|
||||
|
||||
inline const char *hexByte(uint8_t value) {
|
||||
static const auto table = [] {
|
||||
std::array<char[3], 256> t;
|
||||
for (int i = 0; i < 256; ++i) snprintf(t[i], sizeof(t[i]), "%02X", i);
|
||||
return t;
|
||||
}();
|
||||
return table[value];
|
||||
}
|
||||
|
||||
inline std::string toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
std::string hex;
|
||||
hex.reserve(dat.size() * (separator ? 3 : 2));
|
||||
for (size_t i = 0; i < dat.size(); ++i) {
|
||||
if (separator && i) hex += separator;
|
||||
hex += digits[dat[i] >> 4];
|
||||
hex += digits[dat[i] & 0xf];
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
inline std::string toHexString(int value) {
|
||||
char buf[16] = {};
|
||||
snprintf(buf, sizeof(buf), "0x%02X", value);
|
||||
return buf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +1,69 @@
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#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>
|
||||
#ifdef __APPLE__
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
|
||||
#include <QColor>
|
||||
#include <QFontDatabase>
|
||||
#include <QPixmapCache>
|
||||
#include <QPainterPath>
|
||||
#include <unordered_map>
|
||||
#include "common/util.h"
|
||||
|
||||
// SegmentTree
|
||||
static const std::thread::id main_thread_id = std::this_thread::get_id();
|
||||
static std::mutex main_thread_queue_mutex;
|
||||
static std::vector<std::function<void()>> main_thread_queue;
|
||||
|
||||
void SegmentTree::build(const std::vector<QPointF> &arr) {
|
||||
size = arr.size();
|
||||
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);
|
||||
bool utils::isMainThread() { return std::this_thread::get_id() == main_thread_id; }
|
||||
|
||||
void utils::runOnMainThread(std::function<void()> fn) {
|
||||
if (isMainThread()) {
|
||||
fn();
|
||||
} else {
|
||||
std::lock_guard lk(main_thread_queue_mutex);
|
||||
main_thread_queue.push_back(std::move(fn));
|
||||
}
|
||||
}
|
||||
|
||||
void SegmentTree::build_tree(const std::vector<QPointF> &arr, int n, int left, int right) {
|
||||
void utils::drainMainThreadQueue() {
|
||||
std::vector<std::function<void()>> fns;
|
||||
{
|
||||
std::lock_guard lk(main_thread_queue_mutex);
|
||||
fns.swap(main_thread_queue);
|
||||
}
|
||||
for (auto &fn : fns) fn();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void SegmentTree::build(int n, const std::function<double(int)> &y) {
|
||||
size = n;
|
||||
tree.resize(4 * size);
|
||||
if (size > 0) {
|
||||
build_tree(y, 1, 0, size - 1);
|
||||
}
|
||||
}
|
||||
|
||||
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)};
|
||||
}
|
||||
}
|
||||
@@ -56,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);
|
||||
@@ -188,118 +114,131 @@ void UnixSignalHandler::signalHandler(int s) {
|
||||
(void)!::write(sig_fd[0], &s, sizeof(s));
|
||||
}
|
||||
|
||||
// NameValidator
|
||||
|
||||
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
|
||||
ValidState validateNodes(const std::string &input) {
|
||||
if (input.empty()) return ValidState::Intermediate;
|
||||
|
||||
NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {}
|
||||
|
||||
QValidator::State NodeValidator::validate(QString &input, int &pos) const {
|
||||
Q_UNUSED(pos);
|
||||
if (input.isEmpty()) return QValidator::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;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
|
||||
|
||||
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);
|
||||
|
||||
svg_str.replace(0, 7, "<svg");
|
||||
svg_str.replace(svg_str.size() - 9, 9, "</svg>");
|
||||
icons[id] = std::move(svg_str);
|
||||
}
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
namespace utils {
|
||||
@@ -340,9 +279,9 @@ bool getClipboardText(std::string *text) {
|
||||
*text = std::move(out);
|
||||
return true;
|
||||
}
|
||||
has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found
|
||||
has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127;
|
||||
}
|
||||
return has_tool; // tool present but clipboard empty
|
||||
return has_tool;
|
||||
}
|
||||
|
||||
bool setClipboardText(const std::string &text) {
|
||||
@@ -356,190 +295,30 @@ bool setClipboardText(const std::string &text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDarkTheme() {
|
||||
QColor windowColor = QApplication::palette().color(QPalette::Window);
|
||||
return windowColor.lightness() < 128;
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
// IQ.Pilot patch: ToolButton("") (chartswidget.cc) asks for an empty id, so pm can
|
||||
// be null. Painting a null QPixmap is a no-op that logs two QPainter warnings on
|
||||
// every dark-theme start. Upstream candidate.
|
||||
if (dark_theme && !pm.isNull()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) {
|
||||
if (absolute_time) {
|
||||
const auto ms_total = static_cast<int64_t>(std::llround(sec * 1000.0));
|
||||
const std::time_t secs = static_cast<std::time_t>(ms_total / 1000);
|
||||
int millis = static_cast<int>(ms_total % 1000);
|
||||
if (millis < 0) millis = -millis;
|
||||
std::tm tm{};
|
||||
localtime_r(&secs, &tm);
|
||||
char buf[64];
|
||||
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
|
||||
if (include_milliseconds) {
|
||||
return QString::asprintf("%s.%03d", buf, millis);
|
||||
}
|
||||
return QString::fromUtf8(buf);
|
||||
}
|
||||
|
||||
// Relative duration (not wall-clock).
|
||||
const bool show_hours = sec > 60 * 60;
|
||||
int total_ms = static_cast<int>(std::llround(std::max(0.0, sec) * 1000.0));
|
||||
const int hours = total_ms / (3600 * 1000);
|
||||
const int minutes = (total_ms / (60 * 1000)) % 60;
|
||||
const int seconds = (total_ms / 1000) % 60;
|
||||
const int millis = total_ms % 1000;
|
||||
if (show_hours) {
|
||||
return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis)
|
||||
: QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds);
|
||||
}
|
||||
return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis)
|
||||
: QString::asprintf("%02d:%02d", minutes, seconds);
|
||||
}
|
||||
|
||||
} // 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;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%g", num);
|
||||
const char *dot = strpbrk(buf, ".,");
|
||||
return dot ? (int)strlen(dot + 1) : 0;
|
||||
}
|
||||
|
||||
QString signalToolTip(const cabana::Signal *sig) {
|
||||
return QObject::tr(R"(
|
||||
%1<br /><span font-size:small">
|
||||
Start Bit: %2 Size: %3<br />
|
||||
MSB: %4 LSB: %5<br />
|
||||
Little Endian: %6 Signed: %7</span>
|
||||
)").arg(QString::fromStdString(sig->name)).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb)
|
||||
.arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
std::filesystem::path app_dir;
|
||||
std::filesystem::path executableDir() {
|
||||
#ifdef __APPLE__
|
||||
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
|
||||
QApplication tmp(argc, argv);
|
||||
app_dir = QCoreApplication::applicationDirPath().toStdString();
|
||||
if (disable_hidpi) {
|
||||
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit());
|
||||
}
|
||||
#else
|
||||
app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path();
|
||||
#endif
|
||||
|
||||
qputenv("QT_DBL_CLICK_DIST", "150");
|
||||
// ensure the current dir matches the exectuable's directory
|
||||
char buf[PATH_MAX];
|
||||
uint32_t size = sizeof(buf);
|
||||
if (_NSGetExecutablePath(buf, &size) != 0) return {};
|
||||
std::error_code ec;
|
||||
std::filesystem::current_path(app_dir, 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;
|
||||
auto path = std::filesystem::canonical(buf, ec);
|
||||
return (ec ? std::filesystem::path(buf) : path).parent_path();
|
||||
#else
|
||||
return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,209 +1,111 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#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/dbc/dbc.h"
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
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); }
|
||||
|
||||
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;
|
||||
};
|
||||
ValidState validateName(std::string &input);
|
||||
|
||||
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;
|
||||
ValidState validateNodes(const std::string &input);
|
||||
|
||||
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;
|
||||
};
|
||||
ValidState validateNonWhitespace(const std::string &input);
|
||||
|
||||
// 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;
|
||||
};
|
||||
ValidState validateIpAddress(const std::string &input);
|
||||
|
||||
// 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;
|
||||
};
|
||||
ValidState validateDouble(const std::string &input);
|
||||
|
||||
// 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;
|
||||
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 {
|
||||
|
||||
QPixmap icon(const QString &id);
|
||||
bool isMainThread();
|
||||
|
||||
void runOnMainThread(std::function<void()> fn);
|
||||
void drainMainThreadQueue();
|
||||
std::string homePath();
|
||||
std::filesystem::path configPath();
|
||||
bool getClipboardText(std::string *text); // false if no clipboard tool is available
|
||||
bool getClipboardText(std::string *text);
|
||||
bool setClipboardText(const std::string &text);
|
||||
bool isDarkTheme();
|
||||
void setTheme(int theme);
|
||||
QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
|
||||
inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) {
|
||||
auto size = (r.size() - text.size()) / 2;
|
||||
p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text);
|
||||
}
|
||||
inline QString toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
|
||||
static const char digits[] = "0123456789ABCDEF";
|
||||
QString hex;
|
||||
hex.reserve(dat.size() * (separator ? 3 : 2));
|
||||
for (size_t i = 0; i < dat.size(); ++i) {
|
||||
if (separator && i) hex += QLatin1Char(separator);
|
||||
hex += QLatin1Char(digits[dat[i] >> 4]);
|
||||
hex += QLatin1Char(digits[dat[i] & 0xf]);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
std::string bootstrapSvg(const std::string &id);
|
||||
|
||||
|
||||
// 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());
|
||||
|
||||
|
||||
template <typename F>
|
||||
auto guarded(const std::shared_ptr<bool> &alive, F fn) {
|
||||
return [alive = std::weak_ptr<bool>(alive), fn = std::move(fn)](auto &&...args) {
|
||||
if (!alive.expired()) fn(std::forward<decltype(args)>(args)...);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
connect(&settings, &Settings::changed, this, &ToolButton::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); }
|
||||
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.
|
||||
class UnixSignalHandler {
|
||||
public:
|
||||
UnixSignalHandler();
|
||||
UnixSignalHandler(std::function<void()> on_signal);
|
||||
~UnixSignalHandler();
|
||||
static void signalHandler(int s);
|
||||
|
||||
@@ -214,7 +116,4 @@ private:
|
||||
};
|
||||
|
||||
int num_decimals(double num);
|
||||
QString signalToolTip(const cabana::Signal *sig);
|
||||
inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); }
|
||||
void initApp(int argc, char *argv[], bool disable_hidpi = true);
|
||||
QPixmap bootstrapPixmap(const QString &id);
|
||||
std::filesystem::path executableDir();
|
||||
|
||||
Reference in New Issue
Block a user