mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 10:02:05 +08:00
cabana: de-Qt, part 3 (#38360)
This commit is contained in:
@@ -3,6 +3,7 @@ moc_*
|
||||
*.generated.qrc
|
||||
|
||||
assets.cc
|
||||
bootstrap_icons.cc
|
||||
|
||||
_cabana
|
||||
dbc/car_fingerprint_to_dbc.json
|
||||
|
||||
@@ -30,7 +30,7 @@ if arch == "Darwin":
|
||||
]
|
||||
qt_dirs += [f"{qt_env['QTDIR']}/include/Qt{m}" for m in qt_modules]
|
||||
qt_env["LINKFLAGS"] += ["-F" + os.path.join(qt_env['QTDIR'], "lib")]
|
||||
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules] + ["OpenGL"]
|
||||
qt_env["FRAMEWORKS"] += [f"Qt{m}" for m in qt_modules]
|
||||
qt_env.AppendENVPath('PATH', os.path.join(qt_env['QTDIR'], "bin"))
|
||||
else:
|
||||
qt_install_prefix = subprocess.check_output(['qmake', '-query', 'QT_INSTALL_PREFIX'], encoding='utf8').strip()
|
||||
@@ -46,7 +46,7 @@ else:
|
||||
qt_dirs += [f"{qt_install_headers}/QtGui/{qt_gui_dirs[0]}/QtGui", ] if qt_gui_dirs else []
|
||||
qt_dirs += [f"{qt_install_headers}/Qt{m}" for m in qt_modules]
|
||||
|
||||
qt_libs = [f"Qt5{m}" for m in qt_modules] + ["GL"]
|
||||
qt_libs = [f"Qt5{m}" for m in qt_modules]
|
||||
qt_env['QT3DIR'] = qt_env['QTDIR']
|
||||
qt_env.Tool('qt3')
|
||||
|
||||
@@ -67,9 +67,7 @@ base_frameworks = qt_env['FRAMEWORKS']
|
||||
base_libs = [common, messaging, cereal, visionipc, 'm', 'pthread'] + qt_env["LIBS"]
|
||||
|
||||
if arch == "Darwin":
|
||||
base_frameworks += ['QtCharts', 'CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox']
|
||||
else:
|
||||
base_libs.append('Qt5Charts')
|
||||
base_frameworks += ['CoreFoundation', 'CoreVideo', 'CoreMedia', 'IOKit', 'Security', 'VideoToolbox']
|
||||
|
||||
cabana_env = qt_env.Clone()
|
||||
cabana_env['CPPPATH'] += [libusb.INCLUDE_DIR]
|
||||
@@ -79,22 +77,26 @@ cabana_libs = [cereal, messaging, visionipc, replay_lib] + ffmpeg_libs + ['bz2',
|
||||
opendbc_path = '-DOPENDBC_FILE_PATH=\'"%s"\'' % (cabana_env.Dir("../../../opendbc_repo/opendbc/dbc").abspath)
|
||||
cabana_env['CXXFLAGS'] += [opendbc_path]
|
||||
|
||||
def write_assets_qrc(target, source, env):
|
||||
with open(str(source[0])) as f:
|
||||
qrc = f.read()
|
||||
with open(str(target[0]), "w") as f:
|
||||
f.write(qrc.replace("@BOOTSTRAP_ICONS_SVG@", str(bootstrap_icons.SVG_PATH)))
|
||||
# embed the bootstrap icons SVG into the binary
|
||||
def build_bootstrap_icons_src(target, source, env):
|
||||
data = open(str(source[0]), 'rb').read()
|
||||
with open(str(target[0]), 'w') as f:
|
||||
f.write('#include <cstddef>\n')
|
||||
f.write('extern const unsigned char bootstrap_icons_svg[];\n')
|
||||
f.write('extern const size_t bootstrap_icons_svg_len;\n')
|
||||
f.write('const unsigned char bootstrap_icons_svg[] = {\n')
|
||||
for i in range(0, len(data), 32):
|
||||
f.write(','.join(str(b) for b in data[i:i+32]) + ',\n')
|
||||
f.write('};\n')
|
||||
f.write('const size_t bootstrap_icons_svg_len = sizeof(bootstrap_icons_svg);\n')
|
||||
return None
|
||||
|
||||
bootstrap_icons_src = cabana_env.Command('assets/bootstrap_icons.cc', str(bootstrap_icons.SVG_PATH), build_bootstrap_icons_src)
|
||||
|
||||
# build assets
|
||||
assets = "assets/assets.cc"
|
||||
assets_src = cabana_env.Command(
|
||||
"assets/assets.generated.qrc",
|
||||
"assets/assets.qrc",
|
||||
write_assets_qrc,
|
||||
)
|
||||
cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET")
|
||||
cabana_env.Depends(assets_src, str(bootstrap_icons.SVG_PATH))
|
||||
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"]))
|
||||
cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET")
|
||||
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',
|
||||
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc',
|
||||
@@ -104,7 +106,7 @@ cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc'
|
||||
'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc']
|
||||
if arch != "Darwin":
|
||||
cabana_srcs += ['streams/socketcanstream.cc']
|
||||
cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs, LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
cabana_lib = cabana_env.Library("cabana_lib", cabana_srcs + [bootstrap_icons_src], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
|
||||
if GetOption('extras'):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file alias="bootstrap-icons.svg">@BOOTSTRAP_ICONS_SVG@</file>
|
||||
<file>cabana-icon.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
@@ -37,7 +37,7 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) {
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &BinaryView::refresh);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh);
|
||||
|
||||
addShortcuts();
|
||||
setWhatsThis(R"(
|
||||
@@ -66,7 +66,7 @@ void BinaryView::addShortcuts() {
|
||||
QObject::connect(shortcut_delete_backspace, &QShortcut::activated, shortcut_delete_x, &QShortcut::activated);
|
||||
QObject::connect(shortcut_delete_x, &QShortcut::activated, [=]{
|
||||
if (hovered_sig != nullptr) {
|
||||
UndoStack::push(new RemoveSigCommand(model->msg_id, hovered_sig));
|
||||
UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, hovered_sig));
|
||||
hovered_sig = nullptr;
|
||||
}
|
||||
});
|
||||
@@ -126,7 +126,7 @@ void BinaryView::highlight(const cabana::Signal *sig) {
|
||||
}
|
||||
|
||||
void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags flags) {
|
||||
auto index = indexAt(viewport()->mapFromGlobal(QCursor::pos()));
|
||||
auto index = indexAt(last_mouse_pos);
|
||||
if (!anchor_index.isValid() || !index.isValid())
|
||||
return;
|
||||
|
||||
@@ -141,7 +141,7 @@ void BinaryView::setSelection(const QRect &rect, QItemSelectionModel::SelectionF
|
||||
|
||||
void BinaryView::mousePressEvent(QMouseEvent *event) {
|
||||
resize_sig = nullptr;
|
||||
if (auto index = indexAt(event->pos()); index.isValid() && index.column() != 8) {
|
||||
if (auto index = indexAt(last_mouse_pos = event->pos()); index.isValid() && index.column() != 8) {
|
||||
anchor_index = index;
|
||||
auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer();
|
||||
int bit_pos = get_bit_pos(anchor_index);
|
||||
@@ -158,7 +158,7 @@ void BinaryView::mousePressEvent(QMouseEvent *event) {
|
||||
}
|
||||
|
||||
void BinaryView::highlightPosition(const QPoint &pos) {
|
||||
if (auto index = indexAt(viewport()->mapFromGlobal(pos)); index.isValid()) {
|
||||
if (auto index = indexAt(pos); index.isValid()) {
|
||||
auto item = (BinaryViewModel::Item *)index.internalPointer();
|
||||
const cabana::Signal *sig = item->sigs.empty() ? nullptr : item->sigs.back();
|
||||
highlight(sig);
|
||||
@@ -166,7 +166,7 @@ void BinaryView::highlightPosition(const QPoint &pos) {
|
||||
}
|
||||
|
||||
void BinaryView::mouseMoveEvent(QMouseEvent *event) {
|
||||
highlightPosition(event->globalPos());
|
||||
highlightPosition(last_mouse_pos = event->pos());
|
||||
QTableView::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ void BinaryView::mouseReleaseEvent(QMouseEvent *event) {
|
||||
auto sig = resize_sig ? *resize_sig : cabana::Signal{};
|
||||
std::tie(sig.start_bit, sig.size, sig.is_little_endian) = getSelection(release_index);
|
||||
resize_sig ? emit editSignal(resize_sig, sig)
|
||||
: UndoStack::push(new AddSigCommand(model->msg_id, sig));
|
||||
: UndoStack::instance()->push(new AddSigCommand(model->msg_id, sig));
|
||||
} else {
|
||||
auto item = (const BinaryViewModel::Item *)anchor_index.internalPointer();
|
||||
if (item && item->sigs.size() > 0)
|
||||
@@ -208,7 +208,7 @@ void BinaryView::refresh() {
|
||||
resize_sig = nullptr;
|
||||
hovered_sig = nullptr;
|
||||
model->refresh();
|
||||
highlightPosition(QCursor::pos());
|
||||
if (underMouse()) highlightPosition(last_mouse_pos);
|
||||
}
|
||||
|
||||
std::set<const cabana::Signal *> BinaryView::getOverlappingSignals() const {
|
||||
|
||||
@@ -94,6 +94,7 @@ private:
|
||||
void highlightPosition(const QPoint &pt);
|
||||
|
||||
QModelIndex anchor_index;
|
||||
QPoint last_mouse_pos{-1, -1};
|
||||
BinaryViewModel *model;
|
||||
BinaryItemDelegate *delegate;
|
||||
bool is_message_active = false;
|
||||
|
||||
@@ -129,11 +129,10 @@ int parseArgs(int argc, char *argv[], CabanaArgs &args, bool &ok) {
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QCoreApplication::setApplicationName("Cabana");
|
||||
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
|
||||
initApp(argc, argv, false);
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationDisplayName("Cabana");
|
||||
app.setWindowIcon(QIcon(":cabana-icon.png"));
|
||||
//app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui
|
||||
|
||||
UnixSignalHandler signalHandler;
|
||||
utils::setTheme(settings.theme);
|
||||
|
||||
@@ -1,139 +1,40 @@
|
||||
#include "tools/cabana/cameraview.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <OpenGL/gl3.h>
|
||||
#else
|
||||
#include <GLES3/gl3.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QPainter>
|
||||
|
||||
namespace {
|
||||
|
||||
const char frame_vertex_shader[] =
|
||||
#ifdef __APPLE__
|
||||
"#version 330 core\n"
|
||||
#else
|
||||
"#version 300 es\n"
|
||||
#endif
|
||||
"layout(location = 0) in vec4 aPosition;\n"
|
||||
"layout(location = 1) in vec2 aTexCoord;\n"
|
||||
"uniform mat4 uTransform;\n"
|
||||
"out vec2 vTexCoord;\n"
|
||||
"void main() {\n"
|
||||
" gl_Position = uTransform * aPosition;\n"
|
||||
" vTexCoord = aTexCoord;\n"
|
||||
"}\n";
|
||||
|
||||
const char frame_fragment_shader[] =
|
||||
#ifdef __APPLE__
|
||||
"#version 330 core\n"
|
||||
#else
|
||||
"#version 300 es\n"
|
||||
"precision mediump float;\n"
|
||||
#endif
|
||||
"uniform sampler2D uTextureY;\n"
|
||||
"uniform sampler2D uTextureUV;\n"
|
||||
"in vec2 vTexCoord;\n"
|
||||
"out vec4 colorOut;\n"
|
||||
"void main() {\n"
|
||||
" float y = texture(uTextureY, vTexCoord).r;\n"
|
||||
" vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n"
|
||||
" float r = y + 1.402 * uv.y;\n"
|
||||
" float g = y - 0.344 * uv.x - 0.714 * uv.y;\n"
|
||||
" float b = y + 1.772 * uv.x;\n"
|
||||
" colorOut = vec4(r, g, b, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
} // namespace
|
||||
#include "common/yuv.h"
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) :
|
||||
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) {
|
||||
stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) {
|
||||
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||
qRegisterMetaType<std::set<VisionStreamType>>("availableStreams");
|
||||
QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection);
|
||||
QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection);
|
||||
QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection);
|
||||
QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread);
|
||||
}
|
||||
|
||||
CameraWidget::~CameraWidget() {
|
||||
makeCurrent();
|
||||
stopVipcThread();
|
||||
if (isValid()) {
|
||||
glDeleteVertexArrays(1, &frame_vao);
|
||||
glDeleteBuffers(1, &frame_vbo);
|
||||
glDeleteBuffers(1, &frame_ibo);
|
||||
glDeleteTextures(2, textures);
|
||||
shader_program_.reset();
|
||||
}
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
void CameraWidget::initializeGL() {
|
||||
initializeOpenGLFunctions();
|
||||
|
||||
shader_program_ = std::make_unique<QOpenGLShaderProgram>(context());
|
||||
shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader);
|
||||
shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader);
|
||||
shader_program_->link();
|
||||
|
||||
GLint frame_pos_loc = shader_program_->attributeLocation("aPosition");
|
||||
GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord");
|
||||
|
||||
auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f);
|
||||
const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3};
|
||||
const float frame_coords[4][4] = {
|
||||
{-1.0, -1.0, x2, y1}, // bl
|
||||
{-1.0, 1.0, x2, y2}, // tl
|
||||
{ 1.0, 1.0, x1, y2}, // tr
|
||||
{ 1.0, -1.0, x1, y1}, // br
|
||||
};
|
||||
|
||||
glGenVertexArrays(1, &frame_vao);
|
||||
glBindVertexArray(frame_vao);
|
||||
glGenBuffers(1, &frame_vbo);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, frame_vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(frame_pos_loc);
|
||||
glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE,
|
||||
sizeof(frame_coords[0]), (const void *)0);
|
||||
glEnableVertexAttribArray(frame_texcoord_loc);
|
||||
glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE,
|
||||
sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2));
|
||||
glGenBuffers(1, &frame_ibo);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo);
|
||||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
glGenTextures(2, textures);
|
||||
|
||||
shader_program_->bind();
|
||||
shader_program_->setUniformValue("uTextureY", 0);
|
||||
shader_program_->setUniformValue("uTextureUV", 1);
|
||||
shader_program_->release();
|
||||
}
|
||||
|
||||
void CameraWidget::showEvent(QShowEvent *event) {
|
||||
if (!vipc_thread) {
|
||||
if (!vipc_thread.joinable()) {
|
||||
clearFrames();
|
||||
vipc_thread = new QThread();
|
||||
connect(vipc_thread, &QThread::started, [=]() { vipcThread(); });
|
||||
connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater);
|
||||
vipc_thread->start();
|
||||
vipc_exit = false;
|
||||
vipc_thread = std::thread(&CameraWidget::vipcThread, this);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::stopVipcThread() {
|
||||
makeCurrent();
|
||||
if (vipc_thread) {
|
||||
vipc_thread->requestInterruption();
|
||||
vipc_thread->quit();
|
||||
vipc_thread->wait();
|
||||
vipc_thread = nullptr;
|
||||
vipc_exit = true;
|
||||
if (vipc_thread.joinable()) {
|
||||
vipc_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,74 +42,29 @@ void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) {
|
||||
available_streams = streams;
|
||||
}
|
||||
|
||||
void CameraWidget::paintGL() {
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF());
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
||||
void CameraWidget::paintEvent(QPaintEvent *event) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), bg);
|
||||
|
||||
std::lock_guard lk(frame_lock);
|
||||
if (!current_frame_) return;
|
||||
if (rgb_frame.isNull()) return;
|
||||
|
||||
// Scale for aspect ratio
|
||||
float widget_ratio = (float)width() / height();
|
||||
float frame_ratio = (float)stream_width / stream_height;
|
||||
float scale_x = std::min(frame_ratio / widget_ratio, 1.0f);
|
||||
float scale_y = std::min(widget_ratio / frame_ratio, 1.0f);
|
||||
float frame_ratio = (float)rgb_frame.width() / rgb_frame.height();
|
||||
int w = std::lround(width() * std::min(frame_ratio / widget_ratio, 1.0f));
|
||||
int h = std::lround(height() * std::min(widget_ratio / frame_ratio, 1.0f));
|
||||
QRect video_rect((width() - w) / 2, (height() - h) / 2, w, h);
|
||||
|
||||
glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio());
|
||||
|
||||
shader_program_->bind();
|
||||
QMatrix4x4 transform;
|
||||
transform.scale(scale_x, scale_y, 1.0f);
|
||||
shader_program_->setUniformValue("uTransform", transform);
|
||||
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, textures[0]);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y);
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2);
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, textures[1]);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv);
|
||||
|
||||
glBindVertexArray(frame_vao);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr);
|
||||
glBindVertexArray(0);
|
||||
|
||||
// Reset both texture units
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
|
||||
shader_program_->release();
|
||||
}
|
||||
|
||||
void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) {
|
||||
makeCurrent();
|
||||
stream_width = vipc_client->buffers[0].width;
|
||||
stream_height = vipc_client->buffers[0].height;
|
||||
stream_stride = vipc_client->buffers[0].stride;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, textures[0]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
|
||||
assert(glGetError() == GL_NO_ERROR);
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, textures[1]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr);
|
||||
assert(glGetError() == GL_NO_ERROR);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform);
|
||||
if (active_stream_type == VISION_STREAM_DRIVER) {
|
||||
// mirror driver camera horizontally
|
||||
const qreal cx = video_rect.x() + video_rect.width() / 2.0;
|
||||
p.translate(cx, 0);
|
||||
p.scale(-1, 1);
|
||||
p.translate(-cx, 0);
|
||||
}
|
||||
p.drawImage(video_rect, rgb_frame);
|
||||
}
|
||||
|
||||
void CameraWidget::vipcFrameReceived() {
|
||||
@@ -220,7 +76,7 @@ void CameraWidget::vipcThread() {
|
||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
||||
VisionIpcBufExtra frame_meta = {};
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) {
|
||||
while (!vipc_exit) {
|
||||
if (!vipc_client || cur_stream != requested_stream_type) {
|
||||
clearFrames();
|
||||
fprintf(stderr, "connecting to stream %d, was connected to %d\n",
|
||||
@@ -234,23 +90,27 @@ void CameraWidget::vipcThread() {
|
||||
clearFrames();
|
||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name, false);
|
||||
if (streams.empty()) {
|
||||
QThread::msleep(100);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
emit vipcAvailableStreamsUpdated(streams);
|
||||
|
||||
if (!vipc_client->connect(false)) {
|
||||
QThread::msleep(100);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
emit vipcThreadConnected(vipc_client.get());
|
||||
}
|
||||
|
||||
if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) {
|
||||
// NV12 -> RGBA once per frame on the receive thread; paint just draws the image
|
||||
if (rgb_back.width() != (int)buf->width || rgb_back.height() != (int)buf->height) {
|
||||
rgb_back = QImage(buf->width, buf->height, QImage::Format_RGBA8888);
|
||||
}
|
||||
yuv::nv12_to_rgba(buf->y, buf->stride, buf->uv, buf->stride,
|
||||
rgb_back.bits(), rgb_back.bytesPerLine(), buf->width, buf->height);
|
||||
{
|
||||
std::lock_guard lk(frame_lock);
|
||||
current_frame_ = buf;
|
||||
frame_meta_ = frame_meta;
|
||||
rgb_frame.swap(rgb_back);
|
||||
}
|
||||
emit vipcThreadFrameReceived();
|
||||
}
|
||||
@@ -259,6 +119,7 @@ void CameraWidget::vipcThread() {
|
||||
|
||||
void CameraWidget::clearFrames() {
|
||||
std::lock_guard lk(frame_lock);
|
||||
current_frame_ = nullptr;
|
||||
rgb_frame = QImage();
|
||||
rgb_back = QImage();
|
||||
available_streams.clear();
|
||||
}
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QThread>
|
||||
#include <QImage>
|
||||
#include <QWidget>
|
||||
|
||||
#include "msgq/visionipc/visionipc_client.h"
|
||||
|
||||
class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions {
|
||||
class CameraWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using QOpenGLWidget::QOpenGLWidget;
|
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr);
|
||||
~CameraWidget();
|
||||
void setStreamType(VisionStreamType type) { requested_stream_type = type; }
|
||||
@@ -26,37 +24,30 @@ public:
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
void vipcThreadConnected(VisionIpcClient *);
|
||||
void vipcThreadFrameReceived();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>);
|
||||
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void initializeGL() override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override { stopVipcThread(); }
|
||||
void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); }
|
||||
void vipcThread();
|
||||
void clearFrames();
|
||||
|
||||
GLuint frame_vao, frame_vbo, frame_ibo;
|
||||
GLuint textures[2];
|
||||
std::unique_ptr<QOpenGLShaderProgram> shader_program_;
|
||||
QColor bg = Qt::black;
|
||||
QImage rgb_frame; // written by vipc thread, drawn by GUI thread; guarded by frame_lock
|
||||
QImage rgb_back; // vipc thread only
|
||||
|
||||
std::string stream_name;
|
||||
int stream_width = 0;
|
||||
int stream_height = 0;
|
||||
int stream_stride = 0;
|
||||
std::atomic<VisionStreamType> active_stream_type;
|
||||
std::atomic<VisionStreamType> requested_stream_type;
|
||||
std::set<VisionStreamType> available_streams;
|
||||
QThread *vipc_thread = nullptr;
|
||||
std::recursive_mutex frame_lock;
|
||||
VisionBuf* current_frame_ = nullptr;
|
||||
VisionIpcBufExtra frame_meta_ = {};
|
||||
std::thread vipc_thread;
|
||||
std::atomic<bool> vipc_exit = false;
|
||||
std::mutex frame_lock;
|
||||
|
||||
protected slots:
|
||||
void vipcConnected(VisionIpcClient *vipc_client);
|
||||
void vipcFrameReceived();
|
||||
void availableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QMenu>
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsProxyWidget>
|
||||
#include <QtCharts/QChartView>
|
||||
#include <QtCharts/QLegendMarker>
|
||||
#include <QtCharts/QLineSeries>
|
||||
#include <QtCharts/QScatterSeries>
|
||||
#include <QtCharts/QValueAxis>
|
||||
using namespace QtCharts;
|
||||
|
||||
#include "tools/cabana/chart/tiplabel.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
@@ -25,7 +18,7 @@ enum class SeriesType {
|
||||
};
|
||||
|
||||
class ChartsWidget;
|
||||
class ChartView : public QChartView {
|
||||
class ChartView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
@@ -38,12 +31,15 @@ public:
|
||||
void updatePlotArea(int left, bool force = false);
|
||||
void showTip(double sec);
|
||||
void hideTip();
|
||||
double secondsAtPoint(const QPointF &pt) const { return chart()->mapToValue(pt).x(); }
|
||||
double secondsAtPoint(const QPointF &pt) const {
|
||||
return x_min + (pt.x() - plot_area.left()) * (x_max - x_min) / std::max(plot_area.width(), 1);
|
||||
}
|
||||
|
||||
struct SigItem {
|
||||
MessageId msg_id;
|
||||
const cabana::Signal *sig = nullptr;
|
||||
QXYSeries *series = nullptr;
|
||||
QColor color;
|
||||
bool visible = true;
|
||||
std::vector<QPointF> vals;
|
||||
std::vector<QPointF> step_vals;
|
||||
QPointF track_pt{};
|
||||
@@ -58,7 +54,6 @@ signals:
|
||||
private slots:
|
||||
void signalUpdated(const cabana::Signal *sig);
|
||||
void manageSignals();
|
||||
void handleMarkerClicked();
|
||||
void msgUpdated(MessageId id);
|
||||
void msgRemoved(MessageId id) { removeIf([=](auto &s) { return s.msg_id.address == id.address && !dbc()->msg(id); }); }
|
||||
void signalRemoved(const cabana::Signal *sig) { removeIf([=](auto &s) { return s.sig == sig; }); }
|
||||
@@ -67,52 +62,65 @@ private:
|
||||
void appendCanEvents(const cabana::Signal *sig, const std::vector<const CanEvent *> &events,
|
||||
std::vector<QPointF> &vals, std::vector<QPointF> &step_vals);
|
||||
void createToolButtons();
|
||||
void addSeries(QXYSeries *series);
|
||||
void contextMenuEvent(QContextMenuEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *ev) override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator(false); }
|
||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
QSize sizeHint() const override;
|
||||
void updateAxisY();
|
||||
void updateTitle();
|
||||
void resetChartCache();
|
||||
void setTheme(QChart::ChartTheme theme);
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void drawForeground(QPainter *painter, const QRectF &rect) override;
|
||||
void drawBackground(QPainter *painter, const QRectF &rect) override;
|
||||
void drawDropIndicator(bool draw) { if (std::exchange(can_drop, draw) != can_drop) viewport()->update(); }
|
||||
void drawStaticLayer(QPainter *painter);
|
||||
void drawAxes(QPainter *painter);
|
||||
void drawLegend(QPainter *painter);
|
||||
void drawSeries(QPainter *painter);
|
||||
void drawForeground(QPainter *painter);
|
||||
void drawSignalValue(QPainter *painter);
|
||||
void drawTimeline(QPainter *painter);
|
||||
void drawRubberBandTimeRange(QPainter *painter);
|
||||
int xAxisPrecision() const;
|
||||
std::tuple<double, double, int> getNiceAxisNumbers(qreal min, qreal max, int tick_count);
|
||||
qreal niceNumber(qreal x, bool ceiling);
|
||||
QXYSeries *createSeries(SeriesType type, QColor color);
|
||||
void setSeriesColor(QXYSeries *, QColor color);
|
||||
void updateSeriesPoints();
|
||||
QColor uniqueColor(QColor color, const cabana::Signal *exclude = nullptr) const;
|
||||
void removeIf(std::function<bool(const SigItem &)> predicate);
|
||||
void takeSignalsFrom(ChartView *source);
|
||||
void setDropHighlight(bool highlight) { if (std::exchange(can_drop, highlight) != highlight) update(); }
|
||||
inline void clearTrackPoints() { for (auto &s : sigs) s.track_pt = {}; }
|
||||
inline qreal xPos(double sec) const { return plot_area.left() + (sec - x_min) / (x_max - x_min) * plot_area.width(); }
|
||||
inline qreal yPos(double val) const { return plot_area.bottom() - (val - y_min) / (y_max - y_min) * plot_area.height(); }
|
||||
|
||||
// layout
|
||||
QRect plot_area;
|
||||
QRect move_icon_rect;
|
||||
std::vector<QRect> legend_rects;
|
||||
// axes
|
||||
double x_min;
|
||||
double x_max;
|
||||
double y_min = 0;
|
||||
double y_max = 1;
|
||||
int y_tick_count = 3;
|
||||
int y_precision = 0;
|
||||
QString y_unit;
|
||||
int y_label_width = 0;
|
||||
int align_to = 0;
|
||||
QValueAxis *axis_x;
|
||||
QValueAxis *axis_y;
|
||||
// interaction
|
||||
enum class MouseMode { None, Rubber, Scrub };
|
||||
MouseMode mouse_mode = MouseMode::None;
|
||||
QPoint press_pos;
|
||||
QRect rubber_rect;
|
||||
bool resume_after_scrub = false;
|
||||
|
||||
QMenu *menu;
|
||||
QAction *split_chart_act;
|
||||
QAction *close_act;
|
||||
QGraphicsPixmapItem *move_icon;
|
||||
QGraphicsProxyWidget *close_btn_proxy;
|
||||
QGraphicsProxyWidget *manage_btn_proxy;
|
||||
ToolButton *manage_btn;
|
||||
ToolButton *close_btn;
|
||||
TipLabel *tip_label;
|
||||
std::vector<SigItem> sigs;
|
||||
double cur_sec = 0;
|
||||
SeriesType series_type = SeriesType::Line;
|
||||
bool is_scrubbing = false;
|
||||
bool resume_after_scrub = false;
|
||||
QPixmap chart_pixmap;
|
||||
bool can_drop = false;
|
||||
double tooltip_x = -1;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QMouseEvent>
|
||||
#include <QScrollBar>
|
||||
#include <QToolBar>
|
||||
|
||||
@@ -72,11 +72,14 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
range_slider_action = toolbar->addWidget(range_slider);
|
||||
|
||||
// zoom controls
|
||||
zoom_undo_stack = new QUndoStack(this);
|
||||
toolbar->addAction(undo_zoom_action = zoom_undo_stack->createUndoAction(this));
|
||||
undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise"));
|
||||
toolbar->addAction(redo_zoom_action = zoom_undo_stack->createRedoAction(this));
|
||||
redo_zoom_action->setIcon(utils::icon("arrow-clockwise"));
|
||||
undo_zoom_action = toolbar->addAction(utils::icon("arrow-counterclockwise"), tr("Undo Zoom"), [this]() { zoom_undo_stack.undo(); });
|
||||
redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); });
|
||||
undo_zoom_action->setEnabled(false);
|
||||
redo_zoom_action->setEnabled(false);
|
||||
zoom_undo_stack.setCallbacks({.index_changed = [this]() {
|
||||
undo_zoom_action->setEnabled(zoom_undo_stack.canUndo());
|
||||
redo_zoom_action->setEnabled(zoom_undo_stack.canRedo());
|
||||
}});
|
||||
reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom")));
|
||||
reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
|
||||
|
||||
@@ -89,8 +92,6 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
tabbar->setAutoHide(true);
|
||||
tabbar->setExpanding(false);
|
||||
tabbar->setDrawBase(true);
|
||||
tabbar->setAcceptDrops(true);
|
||||
tabbar->setChangeCurrentOnDrag(true);
|
||||
tabbar->setUsesScrollButtons(true);
|
||||
main_layout->addWidget(tabbar);
|
||||
|
||||
@@ -105,6 +106,11 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) {
|
||||
charts_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
main_layout->addWidget(charts_scroll);
|
||||
|
||||
// chart drag preview
|
||||
drag_preview = new QLabel(this);
|
||||
drag_preview->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
drag_preview->hide();
|
||||
|
||||
// init settings
|
||||
current_theme = settings.theme;
|
||||
column_count = std::clamp(settings.chart_column_count, 1, MAX_COLUMN_COUNT);
|
||||
@@ -186,7 +192,7 @@ void ChartsWidget::timeRangeChanged(const std::optional<std::pair<double, double
|
||||
|
||||
void ChartsWidget::zoomReset() {
|
||||
can->setTimeRange(std::nullopt);
|
||||
zoom_undo_stack->clear();
|
||||
zoom_undo_stack.clear();
|
||||
}
|
||||
|
||||
QRect ChartsWidget::chartVisibleRect(ChartView *chart) {
|
||||
@@ -256,10 +262,6 @@ void ChartsWidget::settingChanged() {
|
||||
if (std::exchange(current_theme, settings.theme) != current_theme) {
|
||||
undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise"));
|
||||
redo_zoom_action->setIcon(utils::icon("arrow-clockwise"));
|
||||
auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight;
|
||||
for (auto c : charts) {
|
||||
c->setTheme(theme);
|
||||
}
|
||||
}
|
||||
if (range_slider->maximum() != settings.max_cached_minutes * 60) {
|
||||
range_slider->setRange(1, settings.max_cached_minutes * 60);
|
||||
@@ -307,12 +309,8 @@ void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||
int pos = std::find(charts.begin(), charts.end(), src_chart) - charts.begin() + 1;
|
||||
for (auto it = src_chart->sigs.begin() + 1; it != src_chart->sigs.end(); /**/) {
|
||||
auto c = createChart(pos);
|
||||
src_chart->chart()->removeSeries(it->series);
|
||||
|
||||
// Restore to the original color
|
||||
it->series->setColor(toQColor(it->sig->color));
|
||||
|
||||
c->addSeries(it->series);
|
||||
it->color = toQColor(it->sig->color);
|
||||
c->sigs.emplace_back(std::move(*it));
|
||||
c->updateAxisY();
|
||||
c->updateTitle();
|
||||
@@ -320,6 +318,7 @@ void ChartsWidget::splitChart(ChartView *src_chart) {
|
||||
}
|
||||
src_chart->updateAxisY();
|
||||
src_chart->updateTitle();
|
||||
updateState();
|
||||
QTimer::singleShot(0, src_chart, &ChartView::resetChartCache);
|
||||
}
|
||||
}
|
||||
@@ -390,7 +389,87 @@ void ChartsWidget::updateLayout(bool force) {
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::startAutoScroll() {
|
||||
void ChartsWidget::startChartDrag(ChartView *chart, const QPoint &global_pos) {
|
||||
stopAutoScroll();
|
||||
drag = {.source = chart, .press_pos = global_pos};
|
||||
QPixmap px = chart->grab().scaledToWidth(CHART_MIN_WIDTH * chart->devicePixelRatio(), Qt::SmoothTransformation);
|
||||
drag_preview->setPixmap(px);
|
||||
drag_preview->resize(px.size() / px.devicePixelRatio());
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartMove(const QPoint &global_pos) {
|
||||
if (!drag.active) {
|
||||
if ((global_pos - drag.press_pos).manhattanLength() < QApplication::startDragDistance()) return;
|
||||
drag.active = true;
|
||||
drag_preview->show();
|
||||
drag_preview->raise();
|
||||
}
|
||||
drag_preview->move(mapFromGlobal(global_pos) + QPoint(5, 5));
|
||||
|
||||
// hovering a tab switches to it so the chart can be dropped into another tab
|
||||
int tab = tabbar->tabAt(tabbar->mapFromGlobal(global_pos));
|
||||
if (tab >= 0 && tab != tabbar->currentIndex()) {
|
||||
tabbar->setCurrentIndex(tab);
|
||||
}
|
||||
|
||||
const QPoint container_pos = charts_container->mapFromGlobal(global_pos);
|
||||
ChartView *target = nullptr;
|
||||
for (auto c : currentCharts()) {
|
||||
if (c != drag.source && c->isVisible() && c->geometry().contains(container_pos)) {
|
||||
target = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::exchange(drop_target, target) != target) {
|
||||
for (auto c : charts) c->setDropHighlight(c == target);
|
||||
}
|
||||
bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos));
|
||||
bool on_background = !target && in_viewport && !charts_container->childAt(container_pos);
|
||||
charts_container->drawDropIndicator(on_background ? container_pos : QPoint());
|
||||
|
||||
if (in_viewport) {
|
||||
startAutoScroll(global_pos);
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::cancelChartDrag() {
|
||||
drag = {};
|
||||
stopAutoScroll();
|
||||
drag_preview->hide();
|
||||
charts_container->drawDropIndicator({});
|
||||
if (auto target = std::exchange(drop_target, nullptr)) target->setDropHighlight(false);
|
||||
}
|
||||
|
||||
void ChartsWidget::dragChartRelease(const QPoint &global_pos) {
|
||||
ChartView *source = drag.source;
|
||||
bool active = drag.active;
|
||||
ChartView *target = drop_target;
|
||||
cancelChartDrag();
|
||||
if (!active) return;
|
||||
|
||||
const QPoint container_pos = charts_container->mapFromGlobal(global_pos);
|
||||
bool in_viewport = charts_scroll->viewport()->rect().contains(charts_scroll->viewport()->mapFromGlobal(global_pos));
|
||||
if (target) {
|
||||
// merge source into target
|
||||
target->takeSignalsFrom(source);
|
||||
} else if (in_viewport && !charts_container->childAt(container_pos)) {
|
||||
// reorder within the current tab
|
||||
auto w = charts_container->getDropAfter(container_pos);
|
||||
if (w != source) {
|
||||
for (auto &[_, list] : tab_charts) {
|
||||
list.erase(std::remove(list.begin(), list.end(), source), list.end());
|
||||
}
|
||||
auto &cur = currentCharts();
|
||||
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
|
||||
cur.insert(cur.begin() + to, source);
|
||||
updateLayout(true);
|
||||
updateTabBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::startAutoScroll(const QPoint &global_pos) {
|
||||
auto_scroll_pos = global_pos;
|
||||
auto_scroll_timer->start(50);
|
||||
}
|
||||
|
||||
@@ -406,7 +485,7 @@ void ChartsWidget::doAutoScroll() {
|
||||
}
|
||||
|
||||
int value = scroll->value();
|
||||
QPoint pos = charts_scroll->viewport()->mapFromGlobal(QCursor::pos());
|
||||
QPoint pos = charts_scroll->viewport()->mapFromGlobal(auto_scroll_pos);
|
||||
QRect area = charts_scroll->viewport()->rect();
|
||||
|
||||
if (pos.y() - area.top() < settings.chart_height / 2) {
|
||||
@@ -414,16 +493,11 @@ void ChartsWidget::doAutoScroll() {
|
||||
} else if (area.bottom() - pos.y() < settings.chart_height / 2) {
|
||||
scroll->setValue(value + auto_scroll_count);
|
||||
}
|
||||
bool vertical_unchanged = value == scroll->value();
|
||||
if (vertical_unchanged) {
|
||||
if (value == scroll->value()) {
|
||||
stopAutoScroll();
|
||||
} else {
|
||||
// mouseMoveEvent to updates the drag-selection rectangle
|
||||
const QPoint globalPos = charts_scroll->viewport()->mapToGlobal(pos);
|
||||
const QPoint windowPos = charts_scroll->window()->mapFromGlobal(globalPos);
|
||||
QMouseEvent mm(QEvent::MouseMove, pos, windowPos, globalPos,
|
||||
Qt::NoButton, Qt::LeftButton, Qt::NoModifier, Qt::MouseEventSynthesizedByQt);
|
||||
QApplication::sendEvent(charts_scroll->viewport(), &mm);
|
||||
} else if (chartDragActive()) {
|
||||
// refresh the drop indicator/target at the new scroll position
|
||||
dragChartMove(auto_scroll_pos);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,11 +514,14 @@ void ChartsWidget::newChart() {
|
||||
for (auto it : items) {
|
||||
c->addSignal(it->msg_id, it->sig);
|
||||
}
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsWidget::removeChart(ChartView *chart) {
|
||||
if (drag.source == chart) cancelChartDrag();
|
||||
if (drop_target == chart) drop_target = nullptr;
|
||||
charts.erase(std::remove(charts.begin(), charts.end(), chart), charts.end());
|
||||
chart->deleteLater();
|
||||
for (auto &[_, list] : tab_charts) {
|
||||
@@ -484,6 +561,19 @@ void ChartsWidget::alignCharts() {
|
||||
}
|
||||
|
||||
bool ChartsWidget::eventFilter(QObject *o, QEvent *e) {
|
||||
// route all mouse events to the chart drag, even when the source chart is hidden by a tab switch
|
||||
if (chartDragActive()) {
|
||||
if (e->type() == QEvent::MouseMove) {
|
||||
dragChartMove(static_cast<QMouseEvent *>(e)->globalPos());
|
||||
return true;
|
||||
} else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(e)->button() == Qt::LeftButton) {
|
||||
dragChartRelease(static_cast<QMouseEvent *>(e)->globalPos());
|
||||
return false; // let the release through so Qt clears the implicit mouse grab
|
||||
} else if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonRelease) {
|
||||
return true; // swallow other buttons during the drag
|
||||
}
|
||||
}
|
||||
|
||||
if (!value_tip_visible_) return false;
|
||||
|
||||
if (e->type() == QEvent::MouseMove) {
|
||||
@@ -492,7 +582,7 @@ bool ChartsWidget::eventFilter(QObject *o, QEvent *e) {
|
||||
|
||||
for (const auto &c : charts) {
|
||||
auto local_pos = c->mapFromGlobal(global_pos);
|
||||
if (c->chart()->plotArea().contains(local_pos)) {
|
||||
if (c->plot_area.contains(local_pos)) {
|
||||
if (on_tip) {
|
||||
showValueTip(c->secondsAtPoint(local_pos));
|
||||
}
|
||||
@@ -524,13 +614,14 @@ bool ChartsWidget::event(QEvent *event) {
|
||||
break;
|
||||
case QEvent::WindowDeactivate:
|
||||
case QEvent::FocusOut:
|
||||
if (chartDragActive()) cancelChartDrag();
|
||||
showValueTip(-1);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (back_button) {
|
||||
zoom_undo_stack->undo();
|
||||
zoom_undo_stack.undo();
|
||||
return true; // Return true since the event has been handled
|
||||
}
|
||||
return QFrame::event(event);
|
||||
@@ -539,7 +630,6 @@ bool ChartsWidget::event(QEvent *event) {
|
||||
// ChartsContainer
|
||||
|
||||
ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent), QWidget(parent) {
|
||||
setAcceptDrops(true);
|
||||
setBackgroundRole(QPalette::Window);
|
||||
QVBoxLayout *charts_main_layout = new QVBoxLayout(this);
|
||||
charts_main_layout->setContentsMargins(0, CHART_SPACING, 0, CHART_SPACING);
|
||||
@@ -549,32 +639,6 @@ ChartsContainer::ChartsContainer(ChartsWidget *parent) : charts_widget(parent),
|
||||
charts_main_layout->addStretch(0);
|
||||
}
|
||||
|
||||
void ChartsContainer::dragEnterEvent(QDragEnterEvent *event) {
|
||||
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
|
||||
event->acceptProposedAction();
|
||||
drawDropIndicator(event->pos());
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsContainer::dropEvent(QDropEvent *event) {
|
||||
if (event->mimeData()->hasFormat(CHART_MIME_TYPE)) {
|
||||
auto w = getDropAfter(event->pos());
|
||||
auto chart = qobject_cast<ChartView *>(event->source());
|
||||
if (w != chart) {
|
||||
for (auto &[_, list] : charts_widget->tab_charts) {
|
||||
list.erase(std::remove(list.begin(), list.end(), chart), list.end());
|
||||
}
|
||||
auto &cur = charts_widget->currentCharts();
|
||||
int to = w ? std::find(cur.begin(), cur.end(), w) - cur.begin() + 1 : 0;
|
||||
cur.insert(cur.begin() + to, chart);
|
||||
charts_widget->updateLayout(true);
|
||||
charts_widget->updateTabBar();
|
||||
event->acceptProposedAction();
|
||||
}
|
||||
drawDropIndicator({});
|
||||
}
|
||||
}
|
||||
|
||||
void ChartsContainer::paintEvent(QPaintEvent *ev) {
|
||||
if (!drop_indictor_pos.isNull() && !childAt(drop_indictor_pos)) {
|
||||
QRect r = geometry();
|
||||
|
||||
@@ -8,15 +8,13 @@
|
||||
#include <QScrollArea>
|
||||
#include <QTimer>
|
||||
#include <QToolBar>
|
||||
#include <QUndoCommand>
|
||||
#include <QUndoStack>
|
||||
|
||||
#include "tools/cabana/chart/signalselector.h"
|
||||
#include "tools/cabana/commands.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
const int CHART_MIN_WIDTH = 300;
|
||||
const QString CHART_MIME_TYPE = "application/x-cabanachartview";
|
||||
|
||||
class ChartView;
|
||||
class ChartsWidget;
|
||||
@@ -24,9 +22,6 @@ class ChartsWidget;
|
||||
class ChartsContainer : public QWidget {
|
||||
public:
|
||||
ChartsContainer(ChartsWidget *parent);
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
void dropEvent(QDropEvent *event) override;
|
||||
void dragLeaveEvent(QDragLeaveEvent *event) override { drawDropIndicator({}); }
|
||||
void drawDropIndicator(const QPoint &pt) { drop_indictor_pos = pt; update(); }
|
||||
void paintEvent(QPaintEvent *ev) override;
|
||||
ChartView *getDropAfter(const QPoint &pos) const;
|
||||
@@ -69,7 +64,12 @@ private:
|
||||
void eventsMerged(const MessageEventsMap &new_events);
|
||||
void updateState();
|
||||
void zoomReset();
|
||||
void startAutoScroll();
|
||||
void startChartDrag(ChartView *chart, const QPoint &global_pos);
|
||||
void dragChartMove(const QPoint &global_pos);
|
||||
void dragChartRelease(const QPoint &global_pos);
|
||||
void cancelChartDrag();
|
||||
bool chartDragActive() const { return drag.source != nullptr; }
|
||||
void startAutoScroll(const QPoint &global_pos);
|
||||
void stopAutoScroll();
|
||||
void doAutoScroll();
|
||||
void updateToolBar();
|
||||
@@ -97,7 +97,7 @@ private:
|
||||
QAction *redo_zoom_action;
|
||||
QAction *reset_zoom_action;
|
||||
ToolButton *reset_zoom_btn;
|
||||
QUndoStack *zoom_undo_stack;
|
||||
UndoStack zoom_undo_stack;
|
||||
|
||||
ToolButton *remove_all_btn;
|
||||
std::vector<ChartView *> charts;
|
||||
@@ -110,7 +110,15 @@ private:
|
||||
QAction *columns_action;
|
||||
int column_count = 1;
|
||||
int current_column_count = 0;
|
||||
struct ChartDrag {
|
||||
ChartView *source = nullptr;
|
||||
QPoint press_pos; // global
|
||||
bool active = false;
|
||||
} drag;
|
||||
QLabel *drag_preview;
|
||||
ChartView *drop_target = nullptr;
|
||||
int auto_scroll_count = 0;
|
||||
QPoint auto_scroll_pos;
|
||||
QTimer *auto_scroll_timer;
|
||||
QTimer *align_timer;
|
||||
int current_theme = 0;
|
||||
@@ -119,11 +127,10 @@ private:
|
||||
friend class ChartsContainer;
|
||||
};
|
||||
|
||||
class ZoomCommand : public QUndoCommand {
|
||||
class ZoomCommand : public UndoCommand {
|
||||
public:
|
||||
ZoomCommand(std::pair<double, double> range) : range(range), QUndoCommand() {
|
||||
ZoomCommand(std::pair<double, double> range) : range(range) {
|
||||
prev_range = can->timeRange();
|
||||
setText(QObject::tr("Zoom to %1-%2").arg(range.first, 0, 'f', 2).arg(range.second, 0, 'f', 2));
|
||||
}
|
||||
void undo() override { can->setTimeRange(prev_range); }
|
||||
void redo() override { can->setTimeRange(range); }
|
||||
|
||||
@@ -1,20 +1,81 @@
|
||||
#include <QApplication>
|
||||
|
||||
#include "tools/cabana/commands.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
// UndoStack
|
||||
|
||||
void UndoStack::push(UndoCommand *cmd) {
|
||||
commands_.resize(index_); // drop any redoable commands
|
||||
if (clean_index_ > index_) clean_index_ = -1;
|
||||
commands_.emplace_back(cmd);
|
||||
cmd->redo();
|
||||
setIndex(index_ + 1);
|
||||
}
|
||||
|
||||
void UndoStack::undo() {
|
||||
if (!canUndo()) return;
|
||||
commands_[index_ - 1]->undo();
|
||||
setIndex(index_ - 1);
|
||||
}
|
||||
|
||||
void UndoStack::redo() {
|
||||
if (!canRedo()) return;
|
||||
commands_[index_]->redo();
|
||||
setIndex(index_ + 1);
|
||||
}
|
||||
|
||||
void UndoStack::clear() {
|
||||
bool was_clean = isClean();
|
||||
commands_.clear();
|
||||
index_ = clean_index_ = 0;
|
||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
||||
if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true);
|
||||
}
|
||||
|
||||
void UndoStack::setClean() {
|
||||
if (!isClean()) {
|
||||
clean_index_ = index_;
|
||||
if (callbacks_.clean_changed) callbacks_.clean_changed(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UndoStack::setIndex(int index) {
|
||||
bool was_clean = isClean();
|
||||
index_ = index;
|
||||
if (callbacks_.index_changed) callbacks_.index_changed();
|
||||
if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean());
|
||||
}
|
||||
|
||||
UndoStack *UndoStack::instance() {
|
||||
static UndoStack undo_stack;
|
||||
return &undo_stack;
|
||||
}
|
||||
|
||||
QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) {
|
||||
UndoStack::instance()->setCallbacks({
|
||||
.index_changed = [this]() { emit indexChanged(); },
|
||||
.clean_changed = [this](bool clean) { emit cleanChanged(clean); },
|
||||
});
|
||||
}
|
||||
|
||||
QtUndoNotifier *undoNotifier() {
|
||||
static QtUndoNotifier notifier;
|
||||
return ¬ifier;
|
||||
}
|
||||
|
||||
// EditMsgCommand
|
||||
|
||||
EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size,
|
||||
const std::string &node, const std::string &comment, QUndoCommand *parent)
|
||||
: id(id), new_name(name), new_size(size), new_node(node), new_comment(comment), QUndoCommand(parent) {
|
||||
const std::string &node, const std::string &comment)
|
||||
: id(id), new_name(name), new_size(size), new_node(node), new_comment(comment) {
|
||||
if (auto msg = dbc()->msg(id)) {
|
||||
old_name = msg->name;
|
||||
old_size = msg->size;
|
||||
old_node = msg->transmitter;
|
||||
old_comment = msg->comment;
|
||||
setText(QObject::tr("edit message %1:%2").arg(QString::fromStdString(name)).arg(id.address));
|
||||
text = "edit message " + name + ":" + std::to_string(id.address);
|
||||
} else {
|
||||
setText(QObject::tr("new message %1:%2").arg(QString::fromStdString(name)).arg(id.address));
|
||||
text = "new message " + name + ":" + std::to_string(id.address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +92,10 @@ void EditMsgCommand::redo() {
|
||||
|
||||
// RemoveMsgCommand
|
||||
|
||||
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) {
|
||||
RemoveMsgCommand::RemoveMsgCommand(const MessageId &id) : id(id) {
|
||||
if (auto msg = dbc()->msg(id)) {
|
||||
message = *msg;
|
||||
setText(QObject::tr("remove message %1:%2").arg(QString::fromStdString(message.name)).arg(id.address));
|
||||
text = "remove message " + message.name + ":" + std::to_string(id.address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +114,9 @@ void RemoveMsgCommand::redo() {
|
||||
|
||||
// AddSigCommand
|
||||
|
||||
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent)
|
||||
: id(id), signal(sig), QUndoCommand(parent) {
|
||||
setText(QObject::tr("add signal %1 to %2:%3").arg(QString::fromStdString(sig.name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
|
||||
AddSigCommand::AddSigCommand(const MessageId &id, const cabana::Signal &sig)
|
||||
: id(id), signal(sig) {
|
||||
text = "add signal " + sig.name + " to " + msgName(id) + ":" + std::to_string(id.address);
|
||||
}
|
||||
|
||||
void AddSigCommand::undo() {
|
||||
@@ -75,8 +136,7 @@ void AddSigCommand::redo() {
|
||||
|
||||
// RemoveSigCommand
|
||||
|
||||
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent)
|
||||
: id(id), QUndoCommand(parent) {
|
||||
RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *sig) : id(id) {
|
||||
sigs.push_back(*sig);
|
||||
if (sig->type == cabana::Signal::Type::Multiplexor) {
|
||||
for (const auto &s : dbc()->msg(id)->sigs) {
|
||||
@@ -85,7 +145,7 @@ RemoveSigCommand::RemoveSigCommand(const MessageId &id, const cabana::Signal *si
|
||||
}
|
||||
}
|
||||
}
|
||||
setText(QObject::tr("remove signal %1 from %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
|
||||
text = "remove signal " + sig->name + " from " + msgName(id) + ":" + std::to_string(id.address);
|
||||
}
|
||||
|
||||
void RemoveSigCommand::undo() { for (const auto &s : sigs) dbc()->addSignal(id, s); }
|
||||
@@ -93,8 +153,8 @@ void RemoveSigCommand::redo() { for (const auto &s : sigs) dbc()->removeSignal(i
|
||||
|
||||
// EditSignalCommand
|
||||
|
||||
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent)
|
||||
: id(id), QUndoCommand(parent) {
|
||||
EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig)
|
||||
: id(id) {
|
||||
sigs.push_back({*sig, new_sig});
|
||||
if (sig->type == cabana::Signal::Type::Multiplexor && new_sig.type == cabana::Signal::Type::Normal) {
|
||||
// convert all multiplexed signals to normal signals
|
||||
@@ -108,17 +168,8 @@ EditSignalCommand::EditSignalCommand(const MessageId &id, const cabana::Signal *
|
||||
}
|
||||
}
|
||||
}
|
||||
setText(QObject::tr("edit signal %1 in %2:%3").arg(QString::fromStdString(sig->name)).arg(QString::fromStdString(msgName(id))).arg(id.address));
|
||||
text = "edit signal " + sig->name + " in " + msgName(id) + ":" + std::to_string(id.address);
|
||||
}
|
||||
|
||||
void EditSignalCommand::undo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.second.name, s.first); }
|
||||
void EditSignalCommand::redo() { for (const auto &s : sigs) dbc()->updateSignal(id, s.first.name, s.second); }
|
||||
|
||||
namespace UndoStack {
|
||||
|
||||
QUndoStack *instance() {
|
||||
static QUndoStack *undo_stack = new QUndoStack(qApp);
|
||||
return undo_stack;
|
||||
}
|
||||
|
||||
} // namespace UndoStack
|
||||
|
||||
@@ -1,19 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QUndoCommand>
|
||||
#include <QUndoStack>
|
||||
#include <QObject>
|
||||
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
class EditMsgCommand : public QUndoCommand {
|
||||
class UndoCommand {
|
||||
public:
|
||||
virtual ~UndoCommand() = default;
|
||||
virtual void undo() = 0;
|
||||
virtual void redo() = 0;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
class UndoStack {
|
||||
public:
|
||||
struct Callbacks {
|
||||
std::function<void()> index_changed;
|
||||
std::function<void(bool)> clean_changed;
|
||||
};
|
||||
|
||||
void push(UndoCommand *cmd); // takes ownership and calls redo()
|
||||
void undo();
|
||||
void redo();
|
||||
void clear();
|
||||
void setClean();
|
||||
bool isClean() const { return clean_index_ == index_; }
|
||||
bool canUndo() const { return index_ > 0; }
|
||||
bool canRedo() const { return index_ < (int)commands_.size(); }
|
||||
std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; }
|
||||
std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; }
|
||||
void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); }
|
||||
static UndoStack *instance();
|
||||
|
||||
private:
|
||||
void setIndex(int index);
|
||||
std::vector<std::unique_ptr<UndoCommand>> commands_;
|
||||
int index_ = 0;
|
||||
int clean_index_ = 0;
|
||||
Callbacks callbacks_;
|
||||
};
|
||||
|
||||
// emits Qt signals for the global undo stack
|
||||
class QtUndoNotifier : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit QtUndoNotifier(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void indexChanged();
|
||||
void cleanChanged(bool clean);
|
||||
};
|
||||
|
||||
QtUndoNotifier *undoNotifier();
|
||||
|
||||
class EditMsgCommand : public UndoCommand {
|
||||
public:
|
||||
EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node,
|
||||
const std::string &comment, QUndoCommand *parent = nullptr);
|
||||
const std::string &comment);
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
@@ -23,9 +74,9 @@ private:
|
||||
int old_size = 0, new_size = 0;
|
||||
};
|
||||
|
||||
class RemoveMsgCommand : public QUndoCommand {
|
||||
class RemoveMsgCommand : public UndoCommand {
|
||||
public:
|
||||
RemoveMsgCommand(const MessageId &id, QUndoCommand *parent = nullptr);
|
||||
RemoveMsgCommand(const MessageId &id);
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
@@ -34,9 +85,9 @@ private:
|
||||
cabana::Msg message;
|
||||
};
|
||||
|
||||
class AddSigCommand : public QUndoCommand {
|
||||
class AddSigCommand : public UndoCommand {
|
||||
public:
|
||||
AddSigCommand(const MessageId &id, const cabana::Signal &sig, QUndoCommand *parent = nullptr);
|
||||
AddSigCommand(const MessageId &id, const cabana::Signal &sig);
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
@@ -46,9 +97,9 @@ private:
|
||||
cabana::Signal signal = {};
|
||||
};
|
||||
|
||||
class RemoveSigCommand : public QUndoCommand {
|
||||
class RemoveSigCommand : public UndoCommand {
|
||||
public:
|
||||
RemoveSigCommand(const MessageId &id, const cabana::Signal *sig, QUndoCommand *parent = nullptr);
|
||||
RemoveSigCommand(const MessageId &id, const cabana::Signal *sig);
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
@@ -57,9 +108,9 @@ private:
|
||||
std::vector<cabana::Signal> sigs;
|
||||
};
|
||||
|
||||
class EditSignalCommand : public QUndoCommand {
|
||||
class EditSignalCommand : public UndoCommand {
|
||||
public:
|
||||
EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig, QUndoCommand *parent = nullptr);
|
||||
EditSignalCommand(const MessageId &id, const cabana::Signal *sig, const cabana::Signal &new_sig);
|
||||
void undo() override;
|
||||
void redo() override;
|
||||
|
||||
@@ -67,8 +118,3 @@ private:
|
||||
const MessageId id;
|
||||
std::vector<std::pair<cabana::Signal, cabana::Signal>> sigs; // {old_sig, new_sig}
|
||||
};
|
||||
|
||||
namespace UndoStack {
|
||||
QUndoStack *instance();
|
||||
inline void push(QUndoCommand *cmd) { instance()->push(cmd); }
|
||||
};
|
||||
|
||||
@@ -20,13 +20,8 @@ some rules
|
||||
- `QObject`, `QMetaObject`, `QMetaType`
|
||||
- `QApplication`, `QCoreApplication`, `QGuiApplication`
|
||||
- `QString`, `QStringList`, `QStringBuilder`, `QChar`, `QLatin1Char`
|
||||
- `QByteArray`
|
||||
- `QVariant`
|
||||
- `QVector`, `QMap`, `QSet`, `QPointer`
|
||||
- `QSettings`
|
||||
- `QFile`, `QFileInfo`, `QDir`, `QIODevice`, `QStandardPaths`
|
||||
- `QThread`
|
||||
- `QTimer`, `QBasicTimer`, `QTimerEvent`
|
||||
- `QTimer`
|
||||
- `QWidget`, `QMainWindow`, `QWindow`
|
||||
- `QDialog`, `QDialogButtonBox`, `QMessageBox`, `QProgressDialog`
|
||||
- `QFileDialog`
|
||||
@@ -36,7 +31,7 @@ some rules
|
||||
- `QComboBox`, `QLineEdit`, `QTextEdit`, `QSpinBox`, `QSlider`
|
||||
- `QLabel`, `QGroupBox`, `QFrame`
|
||||
- `QTabBar`, `QTabWidget`, `QSplitter`, `QScrollArea`, `QScrollBar`
|
||||
- `QDockWidget`, `QStatusBar`, `QProgressBar`, `QRubberBand`
|
||||
- `QDockWidget`, `QStatusBar`, `QProgressBar`
|
||||
- `QFormLayout`, `QGridLayout`, `QHBoxLayout`, `QVBoxLayout`
|
||||
- `QSizePolicy`
|
||||
- `QAbstractItemModel`, `QAbstractTableModel`, `QModelIndex`
|
||||
@@ -44,27 +39,15 @@ some rules
|
||||
- `QTableWidget`, `QTableWidgetItem`, `QListWidget`, `QListWidgetItem`
|
||||
- `QItemSelection`, `QItemSelectionModel`, `QItemSelectionRange`
|
||||
- `QHeaderView`, `QStyledItemDelegate`, `QStyleOptionViewItem`
|
||||
- `QValidator`, `QIntValidator`, `QDoubleValidator`
|
||||
- `QValidator`, `QIntValidator`
|
||||
- `QColor`, `QRgb`, `QPalette`
|
||||
- `QBrush`, `QPen`
|
||||
- `QPainter`, `QPainterPath`, `QStylePainter`
|
||||
- `QPixmap`, `QPixmapCache`, `QIcon`, `QStaticText`
|
||||
- `QImage`, `QPixmap`, `QPixmapCache`, `QStaticText`
|
||||
- `QFont`, `QFontDatabase`, `QFontMetrics`, `QTextDocument`
|
||||
- `QStyle`, `QStyleOption`, `QStyleOptionFrame`, `QStyleOptionSlider`
|
||||
- `QPoint`, `QPointF`, `QRect`, `QRectF`, `QRegion`
|
||||
- `QSize`, `QSizeF`
|
||||
- `QCursor`, `QClipboard`, `QScreen`, `QDesktopWidget`
|
||||
- `QEvent`, `QPaintEvent`, `QResizeEvent`, `QShowEvent`, `QCloseEvent`
|
||||
- `QMouseEvent`, `QWheelEvent`, `QNativeGestureEvent`, `QContextMenuEvent`
|
||||
- `QDrag`, `QMimeData`
|
||||
- `QDragEnterEvent`, `QDragLeaveEvent`, `QDragMoveEvent`, `QDropEvent`
|
||||
- `QKeySequence`, `QShortcut`, `QToolTip`
|
||||
- `QChart`, `QChartView`, `QAbstractAxis`, `QValueAxis`
|
||||
- `QXYSeries`, `QLineSeries`, `QScatterSeries`
|
||||
- `QLegend`, `QLegendMarker`
|
||||
- `QGraphicsScene`, `QGraphicsView`, `QGraphicsItemGroup`, `QGraphicsLayout`
|
||||
- `QGraphicsPixmapItem`, `QGraphicsProxyWidget`
|
||||
- `QOpenGLWidget`, `QOpenGLFunctions`
|
||||
- `QOpenGLShader`, `QOpenGLShaderProgram`
|
||||
- `QMatrix4x4`, `QSurfaceFormat`
|
||||
- `QUndoCommand`, `QUndoStack`, `QUndoView`
|
||||
|
||||
@@ -58,7 +58,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
|
||||
QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); });
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, this, &DetailWidget::refresh);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh);
|
||||
QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu);
|
||||
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
|
||||
if (index != -1) {
|
||||
@@ -211,13 +211,13 @@ void DetailWidget::editMsg() {
|
||||
int size = msg ? msg->size : can->lastMessage(msg_id).dat.size();
|
||||
EditMessageDialog dlg(msg_id, QString::fromStdString(msgName(msg_id)), size, this);
|
||||
if (dlg.exec()) {
|
||||
UndoStack::push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(),
|
||||
UndoStack::instance()->push(new EditMsgCommand(msg_id, dlg.name_edit->text().trimmed().toStdString(), dlg.size_spin->value(),
|
||||
dlg.node->text().trimmed().toStdString(), dlg.comment_edit->toPlainText().trimmed().toStdString()));
|
||||
}
|
||||
}
|
||||
|
||||
void DetailWidget::removeMsg() {
|
||||
UndoStack::push(new RemoveMsgCommand(msg_id));
|
||||
UndoStack::instance()->push(new RemoveMsgCommand(msg_id));
|
||||
}
|
||||
|
||||
// EditMessageDialog
|
||||
|
||||
@@ -209,7 +209,7 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) {
|
||||
QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV);
|
||||
QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &HistoryLogModel::reset);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset);
|
||||
QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset);
|
||||
QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); });
|
||||
}
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
#include "tools/cabana/dbc/dbcqt.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDesktopWidget>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMenuBar>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
#include <QResizeEvent>
|
||||
#include <QShortcut>
|
||||
#include <QTextDocument>
|
||||
#include <QUndoView>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidgetAction>
|
||||
|
||||
#include "json11/json11.hpp"
|
||||
#include "tools/cabana/commands.h"
|
||||
@@ -37,14 +35,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
|
||||
createShortcuts();
|
||||
|
||||
// save default window state to allow resetting it
|
||||
default_state = saveState();
|
||||
default_state = utils::toBytes(saveState());
|
||||
|
||||
// restore states
|
||||
restoreGeometry(settings.geometry);
|
||||
if (isMaximized()) {
|
||||
setGeometry(QApplication::desktop()->availableGeometry(this));
|
||||
}
|
||||
restoreState(settings.window_state);
|
||||
// restore states; restoreGeometry() itself corrects stale off-screen geometry
|
||||
restoreGeometry(utils::qbytes(settings.geometry));
|
||||
restoreState(utils::qbytes(settings.window_state));
|
||||
|
||||
// install handlers
|
||||
static auto static_main_win = this;
|
||||
@@ -65,7 +60,7 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
|
||||
QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage);
|
||||
QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::cleanChanged, this, &MainWindow::undoStackCleanChanged);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged);
|
||||
QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus);
|
||||
|
||||
QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); });
|
||||
@@ -73,10 +68,11 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW
|
||||
}
|
||||
|
||||
void MainWindow::loadFingerprints() {
|
||||
QFile json_file(QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json");
|
||||
if (!json_file.open(QIODevice::ReadOnly)) return;
|
||||
std::ifstream json_file((QApplication::applicationDirPath() + "/dbc/car_fingerprint_to_dbc.json").toStdString());
|
||||
if (!json_file) return;
|
||||
const std::string contents{std::istreambuf_iterator<char>(json_file), std::istreambuf_iterator<char>()};
|
||||
std::string err;
|
||||
auto doc = json11::Json::parse(json_file.readAll().toStdString(), err);
|
||||
auto doc = json11::Json::parse(contents, err);
|
||||
if (!err.empty() || !doc.is_object()) return;
|
||||
fingerprint_to_dbc.clear();
|
||||
for (const auto &kv : doc.object_items()) {
|
||||
@@ -108,8 +104,17 @@ void MainWindow::createActions() {
|
||||
file_menu->addSeparator();
|
||||
QMenu *load_opendbc_menu = file_menu->addMenu(tr("Load DBC from commaai/opendbc"));
|
||||
// load_opendbc_menu->setStyleSheet("QMenu { menu-scrollable: true; }");
|
||||
for (const auto &dbc_name : QDir(OPENDBC_FILE_PATH).entryList({"*.dbc"}, QDir::Files, QDir::Name)) {
|
||||
load_opendbc_menu->addAction(dbc_name, [this, name = dbc_name]() { loadDBCFromOpendbc(name); });
|
||||
std::vector<std::string> dbc_names;
|
||||
std::error_code ec;
|
||||
for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) {
|
||||
if (entry.is_regular_file() && entry.path().extension() == ".dbc") {
|
||||
dbc_names.push_back(entry.path().filename().string());
|
||||
}
|
||||
}
|
||||
std::sort(dbc_names.begin(), dbc_names.end());
|
||||
for (const auto &dbc_name : dbc_names) {
|
||||
QString name = QString::fromStdString(dbc_name);
|
||||
load_opendbc_menu->addAction(name, [this, name]() { loadDBCFromOpendbc(name); });
|
||||
}
|
||||
|
||||
file_menu->addAction(tr("Load DBC From Clipboard"), [=]() { loadFromClipboard(); });
|
||||
@@ -127,18 +132,12 @@ void MainWindow::createActions() {
|
||||
|
||||
// Edit Menu
|
||||
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
|
||||
auto undo_act = UndoStack::instance()->createUndoAction(this, tr("&Undo"));
|
||||
undo_act = edit_menu->addAction(tr("&Undo"), []() { UndoStack::instance()->undo(); });
|
||||
undo_act->setShortcuts(QKeySequence::Undo);
|
||||
edit_menu->addAction(undo_act);
|
||||
auto redo_act = UndoStack::instance()->createRedoAction(this, tr("&Redo"));
|
||||
redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); });
|
||||
redo_act->setShortcuts(QKeySequence::Redo);
|
||||
edit_menu->addAction(redo_act);
|
||||
edit_menu->addSeparator();
|
||||
|
||||
QMenu *commands_menu = edit_menu->addMenu(tr("Command &List"));
|
||||
QWidgetAction *commands_act = new QWidgetAction(this);
|
||||
commands_act->setDefaultWidget(new QUndoView(UndoStack::instance()));
|
||||
commands_menu->addAction(commands_act);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions);
|
||||
updateUndoRedoActions();
|
||||
|
||||
// View Menu
|
||||
QMenu *view_menu = menuBar()->addMenu(tr("&View"));
|
||||
@@ -148,7 +147,7 @@ void MainWindow::createActions() {
|
||||
view_menu->addAction(messages_dock->toggleViewAction());
|
||||
view_menu->addAction(video_dock->toggleViewAction());
|
||||
view_menu->addSeparator();
|
||||
view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(default_state); });
|
||||
view_menu->addAction(tr("Reset Window Layout"), [this]() { restoreState(utils::qbytes(default_state)); });
|
||||
|
||||
// Tools Menu
|
||||
tools_menu = menuBar()->addMenu(tr("&Tools"));
|
||||
@@ -195,7 +194,7 @@ void MainWindow::createDockWidgets() {
|
||||
|
||||
video_splitter->addWidget(charts_container);
|
||||
video_splitter->setStretchFactor(1, 1);
|
||||
video_splitter->restoreState(settings.video_splitter_state);
|
||||
video_splitter->restoreState(utils::qbytes(settings.video_splitter_state));
|
||||
video_splitter->handle(1)->setEnabled(!can->liveStreaming());
|
||||
video_dock->setWidget(video_splitter);
|
||||
QObject::connect(charts_widget, &ChartsWidget::toggleChartsDocking, this, &MainWindow::toggleChartsDocking);
|
||||
@@ -226,6 +225,14 @@ void MainWindow::undoStackCleanChanged(bool clean) {
|
||||
setWindowModified(!clean);
|
||||
}
|
||||
|
||||
void MainWindow::updateUndoRedoActions() {
|
||||
auto stack = UndoStack::instance();
|
||||
undo_act->setEnabled(stack->canUndo());
|
||||
undo_act->setText(stack->canUndo() ? tr("&Undo %1").arg(QString::fromStdString(stack->undoText())) : tr("&Undo"));
|
||||
redo_act->setEnabled(stack->canRedo());
|
||||
redo_act->setText(stack->canRedo() ? tr("&Redo %1").arg(QString::fromStdString(stack->redoText())) : tr("&Redo"));
|
||||
}
|
||||
|
||||
void MainWindow::DBCFileChanged() {
|
||||
UndoStack::instance()->clear();
|
||||
|
||||
@@ -306,11 +313,20 @@ void MainWindow::loadDBCFromOpendbc(const QString &name) {
|
||||
}
|
||||
|
||||
void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
|
||||
std::string text;
|
||||
if (!utils::getClipboardText(&text)) {
|
||||
QMessageBox::warning(this, tr("Load From Clipboard"), tr("No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland)."));
|
||||
return;
|
||||
}
|
||||
if (text.empty()) {
|
||||
QMessageBox::warning(this, tr("Load From Clipboard"), tr("Clipboard is empty."));
|
||||
return;
|
||||
}
|
||||
|
||||
closeFile(s);
|
||||
|
||||
QString dbc_str = QGuiApplication::clipboard()->text();
|
||||
std::string error;
|
||||
bool ret = dbc()->open(s, std::string(""), dbc_str.toStdString(), &error);
|
||||
bool ret = dbc()->open(s, std::string(""), text, &error);
|
||||
if (ret && dbc()->nonEmptyDBCCount() > 0) {
|
||||
QMessageBox::information(this, tr("Load From Clipboard"), tr("DBC Successfully Loaded!"));
|
||||
} else {
|
||||
@@ -436,7 +452,7 @@ void MainWindow::saveFile(DBCFile *dbc_file) {
|
||||
|
||||
void MainWindow::saveFileAs(DBCFile *dbc_file) {
|
||||
QString title = tr("Save File (bus: %1)").arg(QString::fromStdString(toString(dbc()->sources(dbc_file))));
|
||||
QString fn = QFileDialog::getSaveFileName(this, title, QDir::cleanPath(QString::fromStdString(settings.last_dir) + "/untitled.dbc"), tr("DBC (*.dbc)"));
|
||||
QString fn = QFileDialog::getSaveFileName(this, title, QString::fromStdString((std::filesystem::path(settings.last_dir) / "untitled.dbc").string()), tr("DBC (*.dbc)"));
|
||||
if (!fn.isEmpty()) {
|
||||
dbc_file->saveAs(fn.toStdString());
|
||||
UndoStack::instance()->setClean();
|
||||
@@ -455,8 +471,11 @@ void MainWindow::saveToClipboard() {
|
||||
|
||||
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
|
||||
assert(dbc_file != nullptr);
|
||||
QGuiApplication::clipboard()->setText(QString::fromStdString(dbc_file->generateDBC()));
|
||||
QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!"));
|
||||
if (utils::setClipboardText(dbc_file->generateDBC())) {
|
||||
QMessageBox::information(this, tr("Copy To Clipboard"), tr("DBC Successfully copied!"));
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Copy To Clipboard"), tr("Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland)."));
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::updateLoadSaveMenus() {
|
||||
@@ -496,7 +515,7 @@ void MainWindow::updateRecentFiles(const QString &fn) {
|
||||
while (settings.recent_files.size() > MAX_RECENT_FILES) {
|
||||
settings.recent_files.pop_back();
|
||||
}
|
||||
settings.last_dir = QFileInfo(fn).absolutePath().toStdString();
|
||||
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
|
||||
}
|
||||
|
||||
void MainWindow::updateRecentFileMenu() {
|
||||
@@ -509,7 +528,7 @@ void MainWindow::updateRecentFileMenu() {
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_recent_files; ++i) {
|
||||
QString text = tr("&%1 %2").arg(i + 1).arg(QFileInfo(QString::fromStdString(settings.recent_files[i])).fileName());
|
||||
QString text = tr("&%1 %2").arg(i + 1).arg(QString::fromStdString(std::filesystem::path(settings.recent_files[i]).filename().string()));
|
||||
open_recent_menu->addAction(text, this, [this, file = settings.recent_files[i]]() { loadFile(QString::fromStdString(file)); });
|
||||
}
|
||||
}
|
||||
@@ -576,16 +595,17 @@ void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
floating_window->deleteLater();
|
||||
|
||||
// save states
|
||||
settings.geometry = saveGeometry();
|
||||
settings.window_state = saveState();
|
||||
settings.geometry = utils::toBytes(saveGeometry());
|
||||
settings.window_state = utils::toBytes(saveState());
|
||||
if (can && !can->liveStreaming()) {
|
||||
settings.video_splitter_state = video_splitter->saveState();
|
||||
settings.video_splitter_state = utils::toBytes(video_splitter->saveState());
|
||||
}
|
||||
if (messages_widget) {
|
||||
settings.message_header_state = messages_widget->saveHeaderState();
|
||||
}
|
||||
|
||||
saveSessionState();
|
||||
settings.save();
|
||||
QWidget::closeEvent(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#include <QProgressBar>
|
||||
#include <QSplitter>
|
||||
#include <QStatusBar>
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "tools/cabana/chart/chartswidget.h"
|
||||
#include "tools/cabana/dbc/dbcmanager.h"
|
||||
@@ -68,6 +70,7 @@ protected:
|
||||
void findSimilarBits();
|
||||
void findSignal();
|
||||
void undoStackCleanChanged(bool clean);
|
||||
void updateUndoRedoActions();
|
||||
void onlineHelp();
|
||||
void toggleFullScreen();
|
||||
void updateStatus();
|
||||
@@ -97,8 +100,10 @@ protected:
|
||||
QAction *save_dbc = nullptr;
|
||||
QAction *save_dbc_as = nullptr;
|
||||
QAction *copy_dbc_to_clipboard = nullptr;
|
||||
QAction *undo_act = nullptr;
|
||||
QAction *redo_act = nullptr;
|
||||
QString car_fingerprint;
|
||||
QByteArray default_state;
|
||||
std::vector<uint8_t> default_state;
|
||||
};
|
||||
|
||||
class HelpOverlay : public QWidget {
|
||||
|
||||
@@ -45,7 +45,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget
|
||||
QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions);
|
||||
QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived);
|
||||
QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified);
|
||||
QObject::connect(UndoStack::instance(), &QUndoStack::indexChanged, model, &MessageListModel::dbcModified);
|
||||
QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified);
|
||||
QObject::connect(model, &MessageListModel::modelReset, [this]() {
|
||||
if (current_msg_id) {
|
||||
selectMessage(*current_msg_id);
|
||||
@@ -212,7 +212,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const {
|
||||
return {};
|
||||
}
|
||||
|
||||
void MessageListModel::setFilterStrings(const QMap<int, QString> &filters) {
|
||||
void MessageListModel::setFilterStrings(const std::map<int, QString> &filters) {
|
||||
filters_ = filters;
|
||||
filterAndSort();
|
||||
}
|
||||
@@ -265,14 +265,14 @@ static bool parseRange(const QString &filter, uint32_t value, int base = 10) {
|
||||
}
|
||||
|
||||
bool MessageListModel::match(const MessageListModel::Item &item) {
|
||||
if (filters_.isEmpty())
|
||||
if (filters_.empty())
|
||||
return true;
|
||||
|
||||
bool match = true;
|
||||
const auto &data = can->lastMessage(item.id);
|
||||
for (auto it = filters_.cbegin(); it != filters_.cend() && match; ++it) {
|
||||
const QString &txt = it.value();
|
||||
switch (it.key()) {
|
||||
const QString &txt = it->second;
|
||||
switch (it->first) {
|
||||
case Column::NAME: {
|
||||
match = item.name.contains(txt, Qt::CaseInsensitive);
|
||||
if (!match) {
|
||||
@@ -388,10 +388,13 @@ void MessageView::drawRow(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
painter->setPen(oldPen);
|
||||
}
|
||||
|
||||
void MessageView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) {
|
||||
void MessageView::setModel(QAbstractItemModel *model) {
|
||||
QTreeView::setModel(model);
|
||||
// Bypass the slow call to QTreeView::dataChanged.
|
||||
// QTreeView::dataChanged will invalidate the height cache and that's what we don't need in MessageView.
|
||||
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
|
||||
QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr);
|
||||
QObject::connect(model, &QAbstractItemModel::dataChanged, this,
|
||||
[this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); });
|
||||
}
|
||||
|
||||
void MessageView::updateBytesSectionSize() {
|
||||
@@ -422,9 +425,9 @@ MessageViewHeader::MessageViewHeader(QWidget *parent) : QHeaderView(Qt::Horizont
|
||||
}
|
||||
|
||||
void MessageViewHeader::updateFilters() {
|
||||
QMap<int, QString> filters;
|
||||
for (int i = 0; i < count(); i++) {
|
||||
if (editors[i] && !editors[i]->text().isEmpty()) {
|
||||
std::map<int, QString> filters;
|
||||
for (int i = 0; i < (int)editors.size(); i++) {
|
||||
if (!editors[i]->text().isEmpty()) {
|
||||
filters[i] = editors[i]->text();
|
||||
}
|
||||
}
|
||||
@@ -433,27 +436,24 @@ void MessageViewHeader::updateFilters() {
|
||||
|
||||
void MessageViewHeader::updateHeaderPositions() {
|
||||
QSize sz = QHeaderView::sizeHint();
|
||||
for (int i = 0; i < count(); i++) {
|
||||
if (editors[i]) {
|
||||
int h = editors[i]->sizeHint().height();
|
||||
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
|
||||
editors[i]->setHidden(isSectionHidden(i));
|
||||
}
|
||||
for (int i = 0; i < (int)editors.size(); i++) {
|
||||
int h = editors[i]->sizeHint().height();
|
||||
editors[i]->setGeometry(sectionViewportPosition(i), sz.height(), sectionSize(i), h);
|
||||
editors[i]->setHidden(isSectionHidden(i));
|
||||
}
|
||||
}
|
||||
|
||||
void MessageViewHeader::updateGeometries() {
|
||||
for (int i = 0; i < count(); i++) {
|
||||
if (!editors[i]) {
|
||||
QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
|
||||
editors[i] = new QLineEdit(this);
|
||||
editors[i]->setClearButtonEnabled(true);
|
||||
editors[i]->setPlaceholderText(tr("Filter %1").arg(column_name));
|
||||
for (int i = (int)editors.size(); i < count(); i++) {
|
||||
QString column_name = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
|
||||
auto edit = new QLineEdit(this);
|
||||
edit->setClearButtonEnabled(true);
|
||||
edit->setPlaceholderText(tr("Filter %1").arg(column_name));
|
||||
|
||||
QObject::connect(editors[i], &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
|
||||
}
|
||||
QObject::connect(edit, &QLineEdit::textChanged, this, &MessageViewHeader::updateFilters);
|
||||
editors.push_back(edit);
|
||||
}
|
||||
setViewportMargins(0, 0, 0, editors[0] ? editors[0]->sizeHint().height() : 0);
|
||||
setViewportMargins(0, 0, 0, !editors.empty() ? editors[0]->sizeHint().height() : 0);
|
||||
|
||||
QHeaderView::updateGeometries();
|
||||
updateHeaderPositions();
|
||||
@@ -461,5 +461,5 @@ void MessageViewHeader::updateGeometries() {
|
||||
|
||||
QSize MessageViewHeader::sizeHint() const {
|
||||
QSize sz = QHeaderView::sizeHint();
|
||||
return editors[0] ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
|
||||
return !editors.empty() ? QSize(sz.width(), sz.height() + editors[0]->height() + 1) : sz;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
@@ -35,7 +37,7 @@ public:
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override { return items_.size(); }
|
||||
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
|
||||
void setFilterStrings(const QMap<int, QString> &filters);
|
||||
void setFilterStrings(const std::map<int, QString> &filters);
|
||||
void showInactiveMessages(bool show);
|
||||
void msgsReceived(const std::set<MessageId> *new_msgs, bool has_new_ids);
|
||||
bool filterAndSort();
|
||||
@@ -56,7 +58,7 @@ private:
|
||||
void sortItems(std::vector<MessageListModel::Item> &items);
|
||||
bool match(const MessageListModel::Item &id);
|
||||
|
||||
QMap<int, QString> filters_;
|
||||
std::map<int, QString> filters_;
|
||||
std::set<MessageId> dbc_messages_;
|
||||
int sort_column = 0;
|
||||
Qt::SortOrder sort_order = Qt::AscendingOrder;
|
||||
@@ -68,11 +70,11 @@ class MessageView : public QTreeView {
|
||||
public:
|
||||
MessageView(QWidget *parent) : QTreeView(parent) {}
|
||||
void updateBytesSectionSize();
|
||||
void setModel(QAbstractItemModel *model) override;
|
||||
|
||||
protected:
|
||||
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
void drawBranches(QPainter *painter, const QRect &rect, const QModelIndex &index) const override {}
|
||||
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
};
|
||||
|
||||
@@ -86,7 +88,7 @@ public:
|
||||
QSize sizeHint() const override;
|
||||
void updateFilters();
|
||||
|
||||
QMap<int, QLineEdit *> editors;
|
||||
std::vector<QLineEdit *> editors;
|
||||
};
|
||||
|
||||
class MessagesWidget : public QWidget {
|
||||
@@ -95,8 +97,13 @@ class MessagesWidget : public QWidget {
|
||||
public:
|
||||
MessagesWidget(QWidget *parent);
|
||||
void selectMessage(const MessageId &message_id);
|
||||
QByteArray saveHeaderState() const { return view->header()->saveState(); }
|
||||
bool restoreHeaderState(const QByteArray &state) const { return view->header()->restoreState(state); }
|
||||
std::vector<uint8_t> saveHeaderState() const {
|
||||
const auto state = view->header()->saveState();
|
||||
return {state.begin(), state.end()};
|
||||
}
|
||||
bool restoreHeaderState(const std::vector<uint8_t> &state) const {
|
||||
return view->header()->restoreState({(const char *)state.data(), (int)state.size()});
|
||||
}
|
||||
void suppressHighlighted();
|
||||
|
||||
signals:
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
#include "tools/cabana/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#endif
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <type_traits>
|
||||
|
||||
#include "json11/json11.hpp"
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
const int MIN_CACHE_MINIUTES = 30;
|
||||
@@ -17,32 +39,442 @@ const int MAX_CACHE_MINIUTES = 120;
|
||||
|
||||
Settings settings;
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path settingsFile() {
|
||||
return utils::configPath() / "cabana.json";
|
||||
}
|
||||
|
||||
struct LoadedSettings {
|
||||
json11::Json::object values;
|
||||
bool exists = false;
|
||||
bool valid = true;
|
||||
};
|
||||
|
||||
class FileLock {
|
||||
public:
|
||||
explicit FileLock(const std::filesystem::path &path) {
|
||||
fd = open(path.c_str(), O_CREAT | O_CLOEXEC, 0600);
|
||||
if (fd < 0 || flock(fd, LOCK_EX) < 0) {
|
||||
fprintf(stderr, "failed to lock Cabana settings %s: %s\n", path.c_str(), strerror(errno));
|
||||
if (fd >= 0) close(fd);
|
||||
fd = -1;
|
||||
}
|
||||
}
|
||||
~FileLock() {
|
||||
if (fd >= 0) close(fd);
|
||||
}
|
||||
bool isLocked() const { return fd >= 0; }
|
||||
|
||||
private:
|
||||
int fd = -1;
|
||||
};
|
||||
|
||||
LoadedSettings loadSettings() {
|
||||
std::ifstream input(settingsFile());
|
||||
if (!input) return {};
|
||||
|
||||
const std::string contents{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
std::string error;
|
||||
auto settings_json = json11::Json::parse(contents, error);
|
||||
if (!error.empty() || !settings_json.is_object()) {
|
||||
fprintf(stderr, "failed to read Cabana settings %s%s%s\n", settingsFile().c_str(), error.empty() ? "" : ": ", error.c_str());
|
||||
return {.exists = true, .valid = false};
|
||||
}
|
||||
return {.values = settings_json.object_items(), .exists = true};
|
||||
}
|
||||
|
||||
bool ensureSettingsDirectory() {
|
||||
const auto path = settingsFile();
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(path.parent_path(), error);
|
||||
if (error) {
|
||||
fprintf(stderr, "failed to create Cabana settings directory %s: %s\n", path.parent_path().c_str(), error.message().c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeAll(int fd, const std::string &data) {
|
||||
size_t written = 0;
|
||||
while (written < data.size()) {
|
||||
ssize_t result = write(fd, data.data() + written, data.size() - written);
|
||||
if (result < 0 && errno == EINTR) continue;
|
||||
if (result <= 0) return false;
|
||||
written += result;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool saveSettings(const json11::Json::object &settings_json) {
|
||||
const auto path = settingsFile();
|
||||
const std::string contents = json11::Json(settings_json).dump();
|
||||
std::string temporary_path = path.string() + ".tmp.XXXXXX";
|
||||
int fd = mkstemp(temporary_path.data());
|
||||
if (fd < 0) {
|
||||
fprintf(stderr, "failed to create temporary Cabana settings %s: %s\n", temporary_path.c_str(), strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = writeAll(fd, contents) && fsync(fd) == 0;
|
||||
if (close(fd) < 0) success = false;
|
||||
if (success && rename(temporary_path.c_str(), path.c_str()) < 0) success = false;
|
||||
|
||||
if (success) {
|
||||
int dir_fd = open(path.parent_path().c_str(), O_RDONLY | O_CLOEXEC);
|
||||
success = dir_fd >= 0 && fsync(dir_fd) == 0;
|
||||
if (dir_fd >= 0 && close(dir_fd) < 0) success = false;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
const int saved_errno = errno;
|
||||
unlink(temporary_path.c_str());
|
||||
fprintf(stderr, "failed to save Cabana settings to %s: %s\n", path.c_str(), strerror(saved_errno));
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool preserveCorruptSettings() {
|
||||
const auto path = settingsFile();
|
||||
auto backup = path;
|
||||
backup += ".corrupt";
|
||||
for (int i = 1; std::filesystem::exists(backup); ++i) {
|
||||
backup = path;
|
||||
backup += ".corrupt." + std::to_string(i);
|
||||
}
|
||||
if (rename(path.c_str(), backup.c_str()) < 0) {
|
||||
fprintf(stderr, "failed to preserve corrupt Cabana settings %s: %s\n", path.c_str(), strerror(errno));
|
||||
return false;
|
||||
}
|
||||
fprintf(stderr, "preserved corrupt Cabana settings at %s\n", backup.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Remove the legacy QSettings migration after users have had time to migrate to cabana.json.
|
||||
struct LegacyValue {
|
||||
std::vector<std::string> strings;
|
||||
std::string bytes;
|
||||
bool is_byte_array = false;
|
||||
};
|
||||
|
||||
using LegacySettings = std::map<std::string, LegacyValue>;
|
||||
|
||||
int hexDigit(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
|
||||
void appendUtf8(std::string &result, uint32_t codepoint) {
|
||||
if (codepoint <= 0x7f) {
|
||||
result.push_back(codepoint);
|
||||
} else if (codepoint <= 0x7ff) {
|
||||
result.push_back(0xc0 | (codepoint >> 6));
|
||||
result.push_back(0x80 | (codepoint & 0x3f));
|
||||
} else if (codepoint <= 0xffff) {
|
||||
result.push_back(0xe0 | (codepoint >> 12));
|
||||
result.push_back(0x80 | ((codepoint >> 6) & 0x3f));
|
||||
result.push_back(0x80 | (codepoint & 0x3f));
|
||||
} else {
|
||||
result.push_back(0xf0 | (codepoint >> 18));
|
||||
result.push_back(0x80 | ((codepoint >> 12) & 0x3f));
|
||||
result.push_back(0x80 | ((codepoint >> 6) & 0x3f));
|
||||
result.push_back(0x80 | (codepoint & 0x3f));
|
||||
}
|
||||
}
|
||||
|
||||
LegacyValue decodeIniValue(std::string_view encoded) {
|
||||
std::vector<std::vector<uint32_t>> decoded(1);
|
||||
std::vector<bool> quoted(1, false);
|
||||
bool in_quotes = false;
|
||||
|
||||
for (size_t i = 0; i < encoded.size();) {
|
||||
char c = encoded[i++];
|
||||
if (c == '"') {
|
||||
in_quotes = !in_quotes;
|
||||
quoted.back() = true;
|
||||
} else if (c == ',' && !in_quotes) {
|
||||
decoded.emplace_back();
|
||||
quoted.push_back(false);
|
||||
while (i < encoded.size() && (encoded[i] == ' ' || encoded[i] == '\t')) ++i;
|
||||
} else if (c == '\\' && i < encoded.size()) {
|
||||
c = encoded[i++];
|
||||
static const std::map<char, char> escapes = {
|
||||
{'a', '\a'}, {'b', '\b'}, {'f', '\f'}, {'n', '\n'}, {'r', '\r'}, {'t', '\t'},
|
||||
{'v', '\v'}, {'"', '"'}, {'?', '?'}, {'\'', '\''}, {'\\', '\\'},
|
||||
};
|
||||
if (auto it = escapes.find(c); it != escapes.end()) {
|
||||
decoded.back().push_back(static_cast<unsigned char>(it->second));
|
||||
} else if (c == 'x' && i < encoded.size() && hexDigit(encoded[i]) >= 0) {
|
||||
uint32_t value = 0;
|
||||
while (i < encoded.size() && hexDigit(encoded[i]) >= 0) value = (value << 4) + hexDigit(encoded[i++]);
|
||||
decoded.back().push_back(value & 0xffff);
|
||||
} else if (c >= '0' && c <= '7') {
|
||||
uint32_t value = c - '0';
|
||||
while (i < encoded.size() && encoded[i] >= '0' && encoded[i] <= '7') value = (value << 3) + (encoded[i++] - '0');
|
||||
decoded.back().push_back(value & 0xffff);
|
||||
}
|
||||
} else {
|
||||
decoded.back().push_back(static_cast<unsigned char>(c));
|
||||
}
|
||||
}
|
||||
|
||||
LegacyValue result;
|
||||
for (size_t i = 0; i < decoded.size(); ++i) {
|
||||
auto &value = decoded[i];
|
||||
if (!quoted[i]) {
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) value.erase(value.begin());
|
||||
while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) value.pop_back();
|
||||
}
|
||||
|
||||
std::string string_value;
|
||||
for (size_t j = 0; j < value.size(); ++j) {
|
||||
uint32_t codepoint = value[j];
|
||||
if (codepoint >= 0xd800 && codepoint <= 0xdbff && j + 1 < value.size() && value[j + 1] >= 0xdc00 && value[j + 1] <= 0xdfff) {
|
||||
codepoint = 0x10000 + ((codepoint - 0xd800) << 10) + (value[++j] - 0xdc00);
|
||||
}
|
||||
appendUtf8(string_value, codepoint);
|
||||
}
|
||||
result.strings.push_back(std::move(string_value));
|
||||
}
|
||||
|
||||
if (result.strings.size() == 1 && result.strings[0] == "@Invalid()") {
|
||||
result.strings.clear();
|
||||
} else if (decoded.size() == 1) {
|
||||
static constexpr std::string_view prefix = "@ByteArray(";
|
||||
const auto &value = decoded[0];
|
||||
if (value.size() >= prefix.size() + 1 && std::equal(prefix.begin(), prefix.end(), value.begin()) && value.back() == ')') {
|
||||
result.is_byte_array = true;
|
||||
result.bytes.reserve(value.size() - prefix.size() - 1);
|
||||
for (size_t i = prefix.size(); i + 1 < value.size(); ++i) result.bytes.push_back(value[i] & 0xff);
|
||||
}
|
||||
}
|
||||
if (!result.is_byte_array) {
|
||||
for (auto &value : result.strings) {
|
||||
if (value.compare(0, 2, "@@") == 0) value.erase(0, 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LegacySettings loadLegacySettings() {
|
||||
auto path = settingsFile();
|
||||
path.replace_filename("cabana.conf");
|
||||
std::ifstream input(path);
|
||||
if (!input) return {};
|
||||
|
||||
LegacySettings settings;
|
||||
bool in_general_section = false;
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
if (!line.empty() && line.back() == '\r') line.pop_back();
|
||||
if (line == "[General]") {
|
||||
in_general_section = true;
|
||||
continue;
|
||||
}
|
||||
if (!line.empty() && line.front() == '[') {
|
||||
in_general_section = false;
|
||||
continue;
|
||||
}
|
||||
if (!in_general_section || line.empty() || line.front() == ';') continue;
|
||||
if (auto separator = line.find('='); separator != std::string::npos) {
|
||||
settings[line.substr(0, separator)] = decodeIniValue(std::string_view(line).substr(separator + 1));
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
std::string cfStringToUtf8(CFStringRef value) {
|
||||
CFIndex length = CFStringGetLength(value);
|
||||
CFIndex size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
std::string result(size, '\0');
|
||||
if (!CFStringGetCString(value, result.data(), size, kCFStringEncodingUTF8)) return {};
|
||||
result.resize(strlen(result.c_str()));
|
||||
return result;
|
||||
}
|
||||
|
||||
LegacyValue cfStringValue(CFStringRef string) {
|
||||
LegacyValue value;
|
||||
if (CFStringHasPrefix(string, CFSTR("@ByteArray(")) && CFStringHasSuffix(string, CFSTR(")"))) {
|
||||
CFRange range{11, CFStringGetLength(string) - 12};
|
||||
std::vector<UniChar> data(range.length);
|
||||
CFStringGetCharacters(string, range, data.data());
|
||||
value.is_byte_array = true;
|
||||
value.bytes.reserve(data.size());
|
||||
for (UniChar c : data) value.bytes.push_back(c & 0xff);
|
||||
} else {
|
||||
std::string string_value = cfStringToUtf8(string);
|
||||
if (string_value.compare(0, 2, "@@") == 0) string_value.erase(0, 1);
|
||||
value.strings.push_back(std::move(string_value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
LegacySettings loadLegacySettings() {
|
||||
LegacySettings settings;
|
||||
CFDictionaryRef values = CFPreferencesCopyMultiple(nullptr, CFSTR("com.cabana"),
|
||||
kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
|
||||
if (values == nullptr) return settings;
|
||||
|
||||
CFIndex count = CFDictionaryGetCount(values);
|
||||
std::vector<const void *> keys(count);
|
||||
std::vector<const void *> objects(count);
|
||||
CFDictionaryGetKeysAndValues(values, keys.data(), objects.data());
|
||||
for (CFIndex i = 0; i < count; ++i) {
|
||||
if (CFGetTypeID(keys[i]) != CFStringGetTypeID()) continue;
|
||||
std::string key = cfStringToUtf8(static_cast<CFStringRef>(keys[i]));
|
||||
CFTypeRef object = objects[i];
|
||||
LegacyValue value;
|
||||
if (CFGetTypeID(object) == CFBooleanGetTypeID()) {
|
||||
value.strings.push_back(CFBooleanGetValue(static_cast<CFBooleanRef>(object)) ? "true" : "false");
|
||||
} else if (CFGetTypeID(object) == CFNumberGetTypeID()) {
|
||||
int number = 0;
|
||||
if (CFNumberGetValue(static_cast<CFNumberRef>(object), kCFNumberIntType, &number)) value.strings.push_back(std::to_string(number));
|
||||
} else if (CFGetTypeID(object) == CFStringGetTypeID()) {
|
||||
value = cfStringValue(static_cast<CFStringRef>(object));
|
||||
} else if (CFGetTypeID(object) == CFDataGetTypeID()) {
|
||||
auto data = static_cast<CFDataRef>(object);
|
||||
value.is_byte_array = true;
|
||||
value.bytes.assign(reinterpret_cast<const char *>(CFDataGetBytePtr(data)), CFDataGetLength(data));
|
||||
} else if (CFGetTypeID(object) == CFArrayGetTypeID()) {
|
||||
auto array = static_cast<CFArrayRef>(object);
|
||||
for (CFIndex j = 0; j < CFArrayGetCount(array); ++j) {
|
||||
CFTypeRef item = CFArrayGetValueAtIndex(array, j);
|
||||
if (CFGetTypeID(item) != CFStringGetTypeID()) {
|
||||
value.strings.clear();
|
||||
break;
|
||||
}
|
||||
auto item_value = cfStringValue(static_cast<CFStringRef>(item));
|
||||
if (item_value.strings.size() != 1) {
|
||||
value.strings.clear();
|
||||
break;
|
||||
}
|
||||
value.strings.push_back(std::move(item_value.strings[0]));
|
||||
}
|
||||
}
|
||||
if (!value.strings.empty() || value.is_byte_array || CFGetTypeID(object) == CFArrayGetTypeID()) {
|
||||
settings.emplace(std::move(key), std::move(value));
|
||||
}
|
||||
}
|
||||
CFRelease(values);
|
||||
return settings;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
void readSetting(QSettings &settings_store, const char *key, T &value) {
|
||||
if (auto stored = settings_store.value(key); stored.canConvert<T>()) value = stored.value<T>();
|
||||
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, T &value) {
|
||||
auto it = legacy_settings.find(key);
|
||||
if (it == legacy_settings.end() || it->second.strings.size() != 1) return;
|
||||
const auto &stored = it->second.strings[0];
|
||||
|
||||
if constexpr (std::is_same_v<T, bool>) {
|
||||
if (stored == "true") value = true;
|
||||
if (stored == "false") value = false;
|
||||
} else if constexpr (std::is_integral_v<T> || std::is_enum_v<T>) {
|
||||
int number = 0;
|
||||
auto [end, error] = std::from_chars(stored.data(), stored.data() + stored.size(), number);
|
||||
if (error == std::errc{} && end == stored.data() + stored.size()) value = static_cast<T>(number);
|
||||
}
|
||||
}
|
||||
|
||||
void readSetting(QSettings &settings_store, const char *key, std::string &value) {
|
||||
value = settings_store.value(key, QString::fromStdString(value)).toString().toStdString();
|
||||
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::string &value) {
|
||||
auto it = legacy_settings.find(key);
|
||||
if (it != legacy_settings.end() && it->second.strings.size() == 1) value = it->second.strings[0];
|
||||
}
|
||||
|
||||
void readSetting(QSettings &settings_store, const char *key, std::vector<std::string> &value) {
|
||||
value.clear();
|
||||
for (const auto &item : settings_store.value(key).toStringList()) value.push_back(item.toStdString());
|
||||
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector<std::string> &value) {
|
||||
auto it = legacy_settings.find(key);
|
||||
if (it != legacy_settings.end() && !it->second.is_byte_array) value = it->second.strings;
|
||||
}
|
||||
|
||||
void readLegacySetting(const LegacySettings &legacy_settings, const char *key, std::vector<uint8_t> &value) {
|
||||
auto it = legacy_settings.find(key);
|
||||
if (it != legacy_settings.end() && it->second.is_byte_array) {
|
||||
value.assign(it->second.bytes.begin(), it->second.bytes.end());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void writeSetting(QSettings &settings_store, const char *key, const T &value) { settings_store.setValue(key, value); }
|
||||
void writeSetting(QSettings &settings_store, const char *key, const std::string &value) { settings_store.setValue(key, QString::fromStdString(value)); }
|
||||
void writeSetting(QSettings &settings_store, const char *key, const std::vector<std::string> &value) {
|
||||
QStringList items;
|
||||
for (const auto &item : value) items.push_back(QString::fromStdString(item));
|
||||
settings_store.setValue(key, items);
|
||||
void readSetting(const json11::Json::object &settings_json, const char *key, T &value) {
|
||||
auto it = settings_json.find(key);
|
||||
if (it == settings_json.end()) return;
|
||||
|
||||
if constexpr (std::is_same_v<T, bool>) {
|
||||
if (it->second.is_bool()) value = it->second.bool_value();
|
||||
} else if constexpr (std::is_integral_v<T>) {
|
||||
if (it->second.is_number()) value = it->second.int_value();
|
||||
} else if constexpr (std::is_enum_v<T>) {
|
||||
if (it->second.is_number()) value = static_cast<T>(it->second.int_value());
|
||||
}
|
||||
}
|
||||
|
||||
template <class SettingOperation>
|
||||
void settingsOp(SettingOperation op) {
|
||||
QSettings s("cabana");
|
||||
void readSetting(const json11::Json::object &settings_json, const char *key, std::string &value) {
|
||||
auto it = settings_json.find(key);
|
||||
if (it != settings_json.end() && it->second.is_string()) value = it->second.string_value();
|
||||
}
|
||||
|
||||
void readSetting(const json11::Json::object &settings_json, const char *key, std::vector<std::string> &value) {
|
||||
auto it = settings_json.find(key);
|
||||
if (it == settings_json.end() || !it->second.is_array()) return;
|
||||
|
||||
std::vector<std::string> stored;
|
||||
for (const auto &item : it->second.array_items()) {
|
||||
if (!item.is_string()) return;
|
||||
stored.push_back(item.string_value());
|
||||
}
|
||||
value = std::move(stored);
|
||||
}
|
||||
|
||||
void readSetting(const json11::Json::object &settings_json, const char *key, std::vector<uint8_t> &value) {
|
||||
auto it = settings_json.find(key);
|
||||
if (it == settings_json.end() || !it->second.is_string()) return;
|
||||
|
||||
const auto &hex = it->second.string_value();
|
||||
if (hex.size() % 2 == 0 && std::all_of(hex.begin(), hex.end(), [](unsigned char c) { return std::isxdigit(c); })) {
|
||||
value.clear();
|
||||
value.reserve(hex.size() / 2);
|
||||
for (size_t i = 0; i < hex.size(); i += 2) {
|
||||
value.push_back((hexDigit(hex[i]) << 4) | hexDigit(hex[i + 1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void writeSetting(json11::Json::object &settings_json, const char *key, const T &value) {
|
||||
if constexpr (std::is_same_v<T, bool>) {
|
||||
settings_json[key] = value;
|
||||
} else if constexpr (std::is_integral_v<T> || std::is_enum_v<T>) {
|
||||
settings_json[key] = static_cast<int>(value);
|
||||
}
|
||||
}
|
||||
|
||||
void writeSetting(json11::Json::object &settings_json, const char *key, const std::string &value) {
|
||||
settings_json[key] = value;
|
||||
}
|
||||
|
||||
void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector<std::string> &value) {
|
||||
settings_json[key] = value;
|
||||
}
|
||||
|
||||
void writeSetting(json11::Json::object &settings_json, const char *key, const std::vector<uint8_t> &value) {
|
||||
static const char digits[] = "0123456789abcdef";
|
||||
std::string hex;
|
||||
hex.reserve(value.size() * 2);
|
||||
for (uint8_t b : value) {
|
||||
hex.push_back(digits[b >> 4]);
|
||||
hex.push_back(digits[b & 0xf]);
|
||||
}
|
||||
settings_json[key] = hex;
|
||||
}
|
||||
|
||||
template <class Store, class SettingOperation>
|
||||
void settingsOp(Store &s, SettingOperation op) {
|
||||
op(s, "absolute_time", settings.absolute_time);
|
||||
op(s, "fps", settings.fps);
|
||||
op(s, "max_cached_minutes", settings.max_cached_minutes);
|
||||
@@ -70,14 +502,38 @@ void settingsOp(SettingOperation op) {
|
||||
op(s, "active_charts", settings.active_charts);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Settings::Settings() {
|
||||
last_dir = last_route_dir = QDir::homePath().toStdString();
|
||||
log_path = (QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/").toStdString();
|
||||
settingsOp([](QSettings &s, const char *key, auto &value) { readSetting(s, key, value); });
|
||||
last_dir = last_route_dir = utils::homePath();
|
||||
log_path = utils::homePath() + "/cabana_live_stream/";
|
||||
const auto stored_settings = loadSettings();
|
||||
if (stored_settings.valid) {
|
||||
if (stored_settings.exists) {
|
||||
settingsOp(stored_settings.values, [](const auto &s, const char *key, auto &value) { readSetting(s, key, value); });
|
||||
} else {
|
||||
auto legacy_settings = loadLegacySettings();
|
||||
settingsOp(legacy_settings, [](const auto &s, const char *key, auto &value) { readLegacySetting(s, key, value); });
|
||||
}
|
||||
}
|
||||
fps = std::clamp(fps, 1, 100);
|
||||
}
|
||||
|
||||
Settings::~Settings() {
|
||||
settingsOp([](QSettings &s, const char *key, const auto &value) { writeSetting(s, key, value); });
|
||||
// Must be called before main() returns: json11's internal statistics are constructed on first
|
||||
// use at runtime, so they are destroyed before this pre-main global. Saving from ~Settings
|
||||
// would use them after destruction and corrupt the heap.
|
||||
void Settings::save() {
|
||||
if (!ensureSettingsDirectory()) return;
|
||||
|
||||
auto lock_path = settingsFile();
|
||||
lock_path += ".lock";
|
||||
FileLock lock(lock_path);
|
||||
if (!lock.isLocked()) return;
|
||||
|
||||
auto stored_settings = loadSettings();
|
||||
if (!stored_settings.valid && !preserveCorruptSettings()) return;
|
||||
settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); });
|
||||
saveSettings(stored_settings.values);
|
||||
}
|
||||
|
||||
// SettingsDlg
|
||||
@@ -121,6 +577,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
|
||||
log_livestream = new QGroupBox(tr("Enable live stream logging"), this);
|
||||
log_livestream->setCheckable(true);
|
||||
log_livestream->setChecked(settings.log_livestream);
|
||||
QHBoxLayout *path_layout = new QHBoxLayout(log_livestream);
|
||||
path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this));
|
||||
log_path->setReadOnly(true);
|
||||
@@ -135,7 +592,7 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) {
|
||||
QObject::connect(browse_btn, &QPushButton::clicked, [this]() {
|
||||
QString fn = QFileDialog::getExistingDirectory(
|
||||
this, tr("Log File Location"),
|
||||
QStandardPaths::writableLocation(QStandardPaths::HomeLocation),
|
||||
QString::fromStdString(utils::homePath()),
|
||||
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
|
||||
if (!fn.isEmpty()) {
|
||||
log_path->setText(fn);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QGroupBox>
|
||||
@@ -14,13 +16,13 @@ class Settings : public QObject, public CabanaSettingsState {
|
||||
|
||||
public:
|
||||
Settings();
|
||||
~Settings();
|
||||
void save();
|
||||
|
||||
// Qt frontend layout state. This intentionally stays outside CabanaSettingsState.
|
||||
QByteArray geometry;
|
||||
QByteArray video_splitter_state;
|
||||
QByteArray window_state;
|
||||
QByteArray message_header_state;
|
||||
std::vector<uint8_t> geometry;
|
||||
std::vector<uint8_t> video_splitter_state;
|
||||
std::vector<uint8_t> window_state;
|
||||
std::vector<uint8_t> message_header_state;
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
@@ -198,7 +198,7 @@ bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s)
|
||||
if (s.is_little_endian != origin_s->is_little_endian) {
|
||||
s.start_bit = flipBitPos(s.start_bit);
|
||||
}
|
||||
UndoStack::push(new EditSignalCommand(msg_id, origin_s, s));
|
||||
UndoStack::instance()->push(new EditSignalCommand(msg_id, origin_s, s));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ void SignalView::rowsChanged() {
|
||||
|
||||
tree->setIndexWidget(index, w);
|
||||
auto sig = model->getItem(index)->sig;
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::push(new RemoveSigCommand(model->msg_id, sig)); });
|
||||
QObject::connect(remove_btn, &QToolButton::clicked, [=]() { UndoStack::instance()->push(new RemoveSigCommand(model->msg_id, sig)); });
|
||||
QObject::connect(plot_btn, &QToolButton::clicked, [=](bool checked) {
|
||||
emit showChart(model->msg_id, sig, checked, QGuiApplication::keyboardModifiers() & Qt::ShiftModifier);
|
||||
});
|
||||
|
||||
@@ -129,9 +129,12 @@ private:
|
||||
// update widget geometries in QTreeView::rowsInserted
|
||||
QTreeView::rowsInserted(parent, start, end);
|
||||
}
|
||||
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) override {
|
||||
void setModel(QAbstractItemModel *model) override {
|
||||
QTreeView::setModel(model);
|
||||
// Bypass the slow call to QTreeView::dataChanged.
|
||||
QAbstractItemView::dataChanged(topLeft, bottomRight, roles);
|
||||
QObject::disconnect(model, &QAbstractItemModel::dataChanged, this, nullptr);
|
||||
QObject::connect(model, &QAbstractItemModel::dataChanged, this,
|
||||
[this](const QModelIndex &tl, const QModelIndex &br, const auto &roles) { QAbstractItemView::dataChanged(tl, br, roles); });
|
||||
}
|
||||
void leaveEvent(QEvent *event) override {
|
||||
emit static_cast<SignalView *>(parentWidget())->highlight(nullptr);
|
||||
|
||||
@@ -75,12 +75,12 @@ protected:
|
||||
const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c);
|
||||
void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size);
|
||||
void waitForSeekFinshed();
|
||||
virtual void updateLastMessages();
|
||||
std::vector<const CanEvent *> all_events_;
|
||||
double current_sec_ = 0;
|
||||
std::optional<std::pair<double, double>> time_range_;
|
||||
|
||||
private:
|
||||
void updateLastMessages();
|
||||
void updateLastMsgsTo(double sec);
|
||||
void updateMasks();
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
#include "tools/cabana/streams/devicestream.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "openpilot/cereal/services.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFileInfo>
|
||||
#include <QFormLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QRadioButton>
|
||||
#include <QThread>
|
||||
|
||||
#include "tools/cabana/utils/util.h"
|
||||
|
||||
@@ -26,6 +27,7 @@ DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(addre
|
||||
}
|
||||
|
||||
DeviceStream::~DeviceStream() {
|
||||
stop();
|
||||
stopBridge();
|
||||
}
|
||||
|
||||
@@ -50,9 +52,8 @@ void DeviceStream::stopBridge() {
|
||||
void DeviceStream::start() {
|
||||
if (!zmq_address.isEmpty()) {
|
||||
stopBridge();
|
||||
QString bridge_path = QFileInfo(QCoreApplication::applicationDirPath() +
|
||||
"/../../openpilot/cereal/messaging/bridge").absoluteFilePath();
|
||||
const std::string path = bridge_path.toStdString();
|
||||
const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) /
|
||||
"../../openpilot/cereal/messaging/bridge").lexically_normal().string();
|
||||
const std::string addr = zmq_address.toStdString();
|
||||
const char *can_filter = "/\"can/\"";
|
||||
|
||||
@@ -108,10 +109,10 @@ void DeviceStream::streamThread() {
|
||||
std::unique_ptr<SubSocket> sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size));
|
||||
assert(sock != NULL);
|
||||
// run as fast as messages come in
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) {
|
||||
while (!exit_) {
|
||||
std::unique_ptr<Message> msg(sock->receive(true));
|
||||
if (!msg) {
|
||||
QThread::msleep(50);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
continue;
|
||||
}
|
||||
handleEvent(kj::ArrayPtr<capnp::word>((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word)));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "tools/cabana/streams/livestream.h"
|
||||
|
||||
#include <QThread>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
@@ -42,37 +42,34 @@ LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) {
|
||||
if (settings.log_livestream) {
|
||||
logger = std::make_unique<Logger>();
|
||||
}
|
||||
stream_thread = new QThread(this);
|
||||
|
||||
QObject::connect(&settings, &Settings::changed, this, &LiveStream::startUpdateTimer);
|
||||
QObject::connect(stream_thread, &QThread::started, [=]() { streamThread(); });
|
||||
QObject::connect(stream_thread, &QThread::finished, stream_thread, &QThread::deleteLater);
|
||||
}
|
||||
|
||||
LiveStream::~LiveStream() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void LiveStream::startUpdateTimer() {
|
||||
update_timer.stop();
|
||||
update_timer.start(1000.0 / settings.fps, this);
|
||||
timer_id = update_timer.timerId();
|
||||
}
|
||||
|
||||
void LiveStream::start() {
|
||||
stream_thread->start();
|
||||
startUpdateTimer();
|
||||
begin_date_time = std::chrono::system_clock::now();
|
||||
fps_ = settings.fps;
|
||||
exit_ = false;
|
||||
stream_thread = std::thread(&LiveStream::streamThread, this);
|
||||
update_thread = std::thread(&LiveStream::updateThread, this);
|
||||
}
|
||||
|
||||
void LiveStream::stop() {
|
||||
if (!stream_thread) return;
|
||||
exit_ = true;
|
||||
if (stream_thread.joinable()) stream_thread.join();
|
||||
if (update_thread.joinable()) update_thread.join();
|
||||
}
|
||||
|
||||
update_timer.stop();
|
||||
stream_thread->requestInterruption();
|
||||
stream_thread->quit();
|
||||
stream_thread->wait();
|
||||
stream_thread = nullptr;
|
||||
void LiveStream::updateThread() {
|
||||
while (!exit_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_));
|
||||
// coalesce: skip the emit if the main thread hasn't processed the previous one yet.
|
||||
if (!update_pending_.exchange(true)) {
|
||||
emit privateUpdateLastMsgsSignal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// called in streamThread
|
||||
@@ -92,23 +89,22 @@ void LiveStream::handleEvent(kj::ArrayPtr<capnp::word> data) {
|
||||
}
|
||||
}
|
||||
|
||||
void LiveStream::timerEvent(QTimerEvent *event) {
|
||||
if (event->timerId() == timer_id) {
|
||||
{
|
||||
// merge events received from live stream thread.
|
||||
std::lock_guard lk(lock);
|
||||
mergeEvents(received_events_);
|
||||
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
|
||||
lastest_event_ts = std::max(lastest_event_ts, last_received_ts);
|
||||
received_events_.clear();
|
||||
}
|
||||
if (!all_events_.empty()) {
|
||||
begin_event_ts = all_events_.front()->mono_time;
|
||||
updateEvents();
|
||||
return;
|
||||
}
|
||||
// called on the main thread by the queued privateUpdateLastMsgsSignal connection
|
||||
void LiveStream::updateLastMessages() {
|
||||
update_pending_ = false;
|
||||
fps_ = settings.fps;
|
||||
{
|
||||
// merge events received from live stream thread.
|
||||
std::lock_guard lk(lock);
|
||||
mergeEvents(received_events_);
|
||||
uint64_t last_received_ts = !received_events_.empty() ? received_events_.back()->mono_time : 0;
|
||||
lastest_event_ts = std::max(lastest_event_ts, last_received_ts);
|
||||
received_events_.clear();
|
||||
}
|
||||
if (!all_events_.empty()) {
|
||||
begin_event_ts = all_events_.front()->mono_time;
|
||||
updateEvents();
|
||||
}
|
||||
QObject::timerEvent(event);
|
||||
}
|
||||
|
||||
void LiveStream::updateEvents() {
|
||||
@@ -138,7 +134,7 @@ void LiveStream::updateEvents() {
|
||||
updateEvent(id, (e->mono_time - begin_event_ts) / 1e9, e->dat, e->size);
|
||||
current_event_ts = e->mono_time;
|
||||
}
|
||||
emit privateUpdateLastMsgsSignal();
|
||||
AbstractStream::updateLastMessages();
|
||||
}
|
||||
|
||||
void LiveStream::seekTo(double sec) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <QBasicTimer>
|
||||
|
||||
#include "tools/cabana/streams/abstractstream.h"
|
||||
|
||||
class LiveStream : public AbstractStream {
|
||||
@@ -29,18 +29,19 @@ protected:
|
||||
virtual void streamThread() = 0;
|
||||
void handleEvent(kj::ArrayPtr<capnp::word> event);
|
||||
|
||||
std::atomic<bool> exit_ = false;
|
||||
|
||||
private:
|
||||
void startUpdateTimer();
|
||||
void timerEvent(QTimerEvent *event) override;
|
||||
void updateThread();
|
||||
void updateLastMessages() override;
|
||||
void updateEvents();
|
||||
|
||||
std::mutex lock;
|
||||
QThread *stream_thread;
|
||||
std::thread stream_thread, update_thread;
|
||||
std::atomic<bool> update_pending_ = false;
|
||||
std::atomic<int> fps_ = 10;
|
||||
std::vector<const CanEvent *> received_events_;
|
||||
|
||||
int timer_id;
|
||||
QBasicTimer update_timer;
|
||||
|
||||
std::chrono::system_clock::time_point begin_date_time;
|
||||
uint64_t begin_event_ts = 0;
|
||||
uint64_t lastest_event_ts = 0;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
#include "tools/cabana/streams/pandastream.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) {
|
||||
@@ -45,13 +46,13 @@ bool PandaStream::connect() {
|
||||
void PandaStream::streamThread() {
|
||||
std::vector<can_frame> raw_can_data;
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) {
|
||||
QThread::msleep(1);
|
||||
while (!exit_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
|
||||
if (!panda->connected()) {
|
||||
fprintf(stderr, "Connection to panda lost. Attempting reconnect.\n");
|
||||
if (!connect()){
|
||||
QThread::msleep(1000);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "tools/cabana/streams/replaystream.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QFileDialog>
|
||||
#include <QGridLayout>
|
||||
@@ -139,7 +141,7 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p
|
||||
QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir));
|
||||
if (!dir.isEmpty()) {
|
||||
route_edit->setText(dir);
|
||||
settings.last_route_dir = QFileInfo(dir).absolutePath().toStdString();
|
||||
settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string();
|
||||
}
|
||||
});
|
||||
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPointer>
|
||||
|
||||
#include "json11/json11.hpp"
|
||||
#include "tools/replay/py_downloader.h"
|
||||
@@ -112,11 +111,10 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
|
||||
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
// Fetch devices
|
||||
QPointer<RoutesDialog> self = this;
|
||||
std::thread([self]() {
|
||||
std::thread([this, alive = std::weak_ptr<bool>(alive_)]() {
|
||||
std::string result = PyDownloader::getDevices();
|
||||
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
|
||||
if (self) self->parseDeviceList(r, response.first, response.second);
|
||||
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() {
|
||||
if (!alive.expired()) parseDeviceList(r, response.first, response.second);
|
||||
}, Qt::QueuedConnection);
|
||||
}).detach();
|
||||
}
|
||||
@@ -156,12 +154,10 @@ void RoutesDialog::fetchRoutes() {
|
||||
}
|
||||
|
||||
int request_id = ++fetch_id_;
|
||||
QPointer<RoutesDialog> self = this;
|
||||
std::thread([self, did, start_ms, end_ms, preserved, request_id]() {
|
||||
std::thread([this, alive = std::weak_ptr<bool>(alive_), did, start_ms, end_ms, preserved, request_id]() {
|
||||
std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved);
|
||||
if (!self || self->fetch_id_ != request_id) return;
|
||||
QMetaObject::invokeMethod(qApp, [self, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
|
||||
if (self && self->fetch_id_ == request_id) self->parseRouteList(r, response.first, response.second);
|
||||
QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() {
|
||||
if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second);
|
||||
}, Qt::QueuedConnection);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
|
||||
@@ -21,4 +23,6 @@ protected:
|
||||
QComboBox *period_selector_;
|
||||
RouteListWidget *route_list_;
|
||||
std::atomic<int> fetch_id_{0};
|
||||
// expires on destruction; guards main-thread callbacks from detached worker threads
|
||||
std::shared_ptr<bool> alive_ = std::make_shared<bool>(true);
|
||||
};
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QThread>
|
||||
|
||||
SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) {
|
||||
if (!available()) {
|
||||
@@ -82,7 +82,7 @@ bool SocketCanStream::connect() {
|
||||
void SocketCanStream::streamThread() {
|
||||
struct canfd_frame frame;
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) {
|
||||
while (!exit_) {
|
||||
ssize_t nbytes = read(sock_fd, &frame, sizeof(frame));
|
||||
if (nbytes <= 0) continue;
|
||||
|
||||
@@ -128,14 +128,12 @@ OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWi
|
||||
void OpenSocketCanWidget::refreshDevices() {
|
||||
device_edit->clear();
|
||||
// Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN)
|
||||
QDir net_dir("/sys/class/net");
|
||||
for (const auto &iface : net_dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
|
||||
QFile type_file(net_dir.filePath(iface) + "/type");
|
||||
if (type_file.open(QIODevice::ReadOnly)) {
|
||||
int type = type_file.readAll().trimmed().toInt();
|
||||
if (type == 280) {
|
||||
device_edit->addItem(iface);
|
||||
}
|
||||
std::error_code ec;
|
||||
for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
|
||||
std::ifstream type_file(entry.path() / "type");
|
||||
int type = 0;
|
||||
if (type_file >> type && type == 280) {
|
||||
device_edit->addItem(QString::fromStdString(entry.path().filename().string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "tools/cabana/streamselector.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
@@ -55,7 +57,7 @@ StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) {
|
||||
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), QString::fromStdString(settings.last_dir), "DBC (*.dbc)");
|
||||
if (!fn.isEmpty()) {
|
||||
dbc_file->setText(fn);
|
||||
settings.last_dir = QFileInfo(fn).absolutePath().toStdString();
|
||||
settings.last_dir = std::filesystem::absolute(fn.toStdString()).parent_path().string();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "tools/cabana/tools/findsignal.h"
|
||||
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
#include <QFormLayout>
|
||||
@@ -210,13 +211,13 @@ void FindSignalDlg::search() {
|
||||
}
|
||||
|
||||
void FindSignalDlg::setInitialSignals() {
|
||||
QSet<ushort> buses;
|
||||
std::set<ushort> buses;
|
||||
for (auto bus : bus_edit->text().trimmed().split(",")) {
|
||||
bus = bus.trimmed();
|
||||
if (!bus.isEmpty()) buses.insert(bus.toUShort());
|
||||
}
|
||||
|
||||
QSet<uint32_t> addresses;
|
||||
std::set<uint32_t> addresses;
|
||||
for (auto addr : address_edit->text().trimmed().split(",")) {
|
||||
addr = addr.trimmed();
|
||||
if (!addr.isEmpty()) addresses.insert(addr.toULong(nullptr, 16));
|
||||
@@ -239,7 +240,7 @@ void FindSignalDlg::setInitialSignals() {
|
||||
model->initial_signals.clear();
|
||||
|
||||
for (const auto &[id, m] : can->lastMessages()) {
|
||||
if ((buses.isEmpty() || buses.contains(id.source)) && (addresses.isEmpty() || addresses.contains(id.address))) {
|
||||
if ((buses.empty() || buses.count(id.source)) && (addresses.empty() || addresses.count(id.address))) {
|
||||
const auto &events = can->events(id);
|
||||
auto e = std::lower_bound(events.cbegin(), events.cend(), first_time, CompareCanEvent());
|
||||
if (e != events.cend()) {
|
||||
@@ -276,7 +277,7 @@ void FindSignalDlg::customMenuRequested(const QPoint &pos) {
|
||||
menu.addAction(tr("Create Signal"));
|
||||
if (menu.exec(view->mapToGlobal(pos))) {
|
||||
auto &s = model->filtered_signals[index.row()];
|
||||
UndoStack::push(new AddSigCommand(s.id, s.sig));
|
||||
UndoStack::instance()->push(new AddSigCommand(s.id, s.sig));
|
||||
emit openMessage(s.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
#include <cerrno>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <csignal>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QDir>
|
||||
#include <QFontDatabase>
|
||||
#include <QPixmapCache>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QFileInfo>
|
||||
#include <QPainterPath>
|
||||
#include <unordered_map>
|
||||
#include "common/util.h"
|
||||
@@ -271,13 +271,13 @@ QValidator::State DoubleValidator::validate(QString &input, int &pos) const {
|
||||
if (input.isEmpty()) return QValidator::Intermediate;
|
||||
|
||||
// Match QString::toDouble(): C locale, no hex floats / inf / nan.
|
||||
const QByteArray bytes = input.toLatin1();
|
||||
const std::string bytes = input.toLatin1().toStdString();
|
||||
// strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not.
|
||||
if (bytes.contains('x') || bytes.contains('X') || bytes.contains('p') || bytes.contains('P')) {
|
||||
if (bytes.find_first_of("xXpP") != std::string::npos) {
|
||||
return QValidator::Invalid;
|
||||
}
|
||||
|
||||
const char *start = bytes.constData();
|
||||
const char *start = bytes.c_str();
|
||||
char *end = nullptr;
|
||||
const double value = std::strtod(start, &end);
|
||||
if (end == start) {
|
||||
@@ -304,6 +304,58 @@ QValidator::State DoubleValidator::validate(QString &input, int &pos) const {
|
||||
|
||||
namespace utils {
|
||||
|
||||
std::string homePath() {
|
||||
const char *home = ::getenv("HOME");
|
||||
return home ? home : "";
|
||||
}
|
||||
|
||||
std::filesystem::path configPath() {
|
||||
#ifdef __APPLE__
|
||||
return std::filesystem::path(homePath()) / "Library/Preferences";
|
||||
#else
|
||||
const char *xdg = ::getenv("XDG_CONFIG_HOME");
|
||||
return (xdg && xdg[0]) ? std::filesystem::path(xdg) : std::filesystem::path(homePath()) / ".config";
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
static const char *clipboard_read_cmds[] = {"pbpaste"};
|
||||
static const char *clipboard_write_cmds[] = {"pbcopy"};
|
||||
#else
|
||||
static const char *clipboard_read_cmds[] = {"wl-paste --no-newline 2>/dev/null", "xclip -selection clipboard -o 2>/dev/null", "xsel -ob 2>/dev/null"};
|
||||
static const char *clipboard_write_cmds[] = {"wl-copy 2>/dev/null", "xclip -selection clipboard 2>/dev/null", "xsel -ib 2>/dev/null"};
|
||||
#endif
|
||||
|
||||
bool getClipboardText(std::string *text) {
|
||||
text->clear();
|
||||
bool has_tool = false;
|
||||
for (const char *cmd : clipboard_read_cmds) {
|
||||
FILE *f = ::popen(cmd, "r");
|
||||
if (!f) continue;
|
||||
std::string out;
|
||||
char buf[4096];
|
||||
for (size_t n; (n = ::fread(buf, 1, sizeof(buf), f)) > 0;) out.append(buf, n);
|
||||
int status = ::pclose(f);
|
||||
if (status == 0) {
|
||||
*text = std::move(out);
|
||||
return true;
|
||||
}
|
||||
has_tool |= WIFEXITED(status) && WEXITSTATUS(status) != 127; // 127: command not found
|
||||
}
|
||||
return has_tool; // tool present but clipboard empty
|
||||
}
|
||||
|
||||
bool setClipboardText(const std::string &text) {
|
||||
std::signal(SIGPIPE, SIG_IGN);
|
||||
for (const char *cmd : clipboard_write_cmds) {
|
||||
FILE *f = ::popen(cmd, "w");
|
||||
if (!f) continue;
|
||||
size_t written = ::fwrite(text.data(), 1, text.size(), f);
|
||||
if (::pclose(f) == 0 && written == text.size()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isDarkTheme() {
|
||||
QColor windowColor = QApplication::palette().color(QPalette::Window);
|
||||
return windowColor.lightness() < 128;
|
||||
@@ -413,20 +465,6 @@ QString signalToolTip(const cabana::Signal *sig) {
|
||||
.arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N");
|
||||
}
|
||||
|
||||
void setSurfaceFormat() {
|
||||
QSurfaceFormat fmt;
|
||||
#ifdef __APPLE__
|
||||
fmt.setVersion(3, 2);
|
||||
fmt.setProfile(QSurfaceFormat::OpenGLContextProfile::CoreProfile);
|
||||
fmt.setRenderableType(QSurfaceFormat::OpenGL);
|
||||
#else
|
||||
fmt.setRenderableType(QSurfaceFormat::OpenGLES);
|
||||
#endif
|
||||
fmt.setSamples(16);
|
||||
fmt.setStencilBufferSize(1);
|
||||
QSurfaceFormat::setDefaultFormat(fmt);
|
||||
}
|
||||
|
||||
void sigTermHandler(int s) {
|
||||
std::signal(s, SIG_DFL);
|
||||
qApp->quit();
|
||||
@@ -437,57 +475,57 @@ void initApp(int argc, char *argv[], bool disable_hidpi) {
|
||||
std::signal(SIGINT, sigTermHandler);
|
||||
std::signal(SIGTERM, sigTermHandler);
|
||||
|
||||
QString app_dir;
|
||||
std::filesystem::path app_dir;
|
||||
#ifdef __APPLE__
|
||||
// Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering
|
||||
QApplication tmp(argc, argv);
|
||||
app_dir = QCoreApplication::applicationDirPath();
|
||||
app_dir = QCoreApplication::applicationDirPath().toStdString();
|
||||
if (disable_hidpi) {
|
||||
qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit());
|
||||
}
|
||||
#else
|
||||
app_dir = QFileInfo(util::readlink("/proc/self/exe").c_str()).path();
|
||||
app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path();
|
||||
#endif
|
||||
|
||||
qputenv("QT_DBL_CLICK_DIST", QByteArray::number(150));
|
||||
qputenv("QT_DBL_CLICK_DIST", "150");
|
||||
// ensure the current dir matches the exectuable's directory
|
||||
QDir::setCurrent(app_dir);
|
||||
|
||||
setSurfaceFormat();
|
||||
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;
|
||||
|
||||
QFile f(":/bootstrap-icons.svg");
|
||||
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
std::string content = f.readAll().toStdString();
|
||||
const std::string sym_open = "<symbol ";
|
||||
const std::string sym_close = "</symbol>";
|
||||
const std::string id_attr = "id=\"";
|
||||
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 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);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
pos = end;
|
||||
}
|
||||
return icons;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QByteArray>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
@@ -132,6 +133,10 @@ public:
|
||||
namespace utils {
|
||||
|
||||
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);
|
||||
QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false);
|
||||
@@ -140,7 +145,22 @@ inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text)
|
||||
p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text);
|
||||
}
|
||||
inline QString toHex(const std::vector<uint8_t> &dat, char separator = '\0') {
|
||||
return QByteArray::fromRawData((const char *)dat.data(), dat.size()).toHex(separator).toUpper();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -356,8 +356,8 @@ void StreamCameraView::parseQLog(std::shared_ptr<LogReader> qlog) {
|
||||
update();
|
||||
}
|
||||
|
||||
void StreamCameraView::paintGL() {
|
||||
CameraWidget::paintGL();
|
||||
void StreamCameraView::paintEvent(QPaintEvent *event) {
|
||||
CameraWidget::paintEvent(event);
|
||||
|
||||
QPainter p(this);
|
||||
bool scrubbing = false;
|
||||
|
||||
@@ -35,7 +35,7 @@ class StreamCameraView : public CameraWidget {
|
||||
|
||||
public:
|
||||
StreamCameraView(std::string stream_name, VisionStreamType stream_type, QWidget *parent = nullptr);
|
||||
void paintGL() override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void parseQLog(std::shared_ptr<LogReader> qlog);
|
||||
|
||||
private:
|
||||
|
||||
@@ -44,7 +44,7 @@ function install_linux_deps() {
|
||||
echo "[ ] system packages already installed t=$SECONDS"
|
||||
elif command -v apt-get > /dev/null 2>&1; then
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git
|
||||
$SUDO apt-get install -y --no-install-recommends ca-certificates build-essential curl libcurl4-openssl-dev locales git xclip wl-clipboard
|
||||
elif command -v dnf > /dev/null 2>&1; then
|
||||
$SUDO dnf install -y ca-certificates gcc gcc-c++ make curl libcurl-devel glibc-langpack-en git
|
||||
elif command -v yum > /dev/null 2>&1; then
|
||||
|
||||
Reference in New Issue
Block a user