openpilot v0.9.6 release
date: 2024-01-12T10:13:37 master commit: ba792d576a49a0899b88a753fa1c52956bedf9e6
This commit is contained in:
435
selfdrive/ui/qt/widgets/cameraview.cc
Normal file
435
selfdrive/ui/qt/widgets/cameraview.cc
Normal file
@@ -0,0 +1,435 @@
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <OpenGL/gl3.h>
|
||||
#else
|
||||
#include <GLES3/gl3.h>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOffscreenSurface>
|
||||
|
||||
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 QCOM2
|
||||
"#version 300 es\n"
|
||||
"#extension GL_OES_EGL_image_external_essl3 : enable\n"
|
||||
"precision mediump float;\n"
|
||||
"uniform samplerExternalOES uTexture;\n"
|
||||
"in vec2 vTexCoord;\n"
|
||||
"out vec4 colorOut;\n"
|
||||
"void main() {\n"
|
||||
" colorOut = texture(uTexture, vTexCoord);\n"
|
||||
"}\n";
|
||||
#else
|
||||
#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";
|
||||
#endif
|
||||
|
||||
mat4 get_driver_view_transform(int screen_width, int screen_height, int stream_width, int stream_height) {
|
||||
const float driver_view_ratio = 2.0;
|
||||
const float yscale = stream_height * driver_view_ratio / stream_width;
|
||||
const float xscale = yscale*screen_height/screen_width*stream_width/stream_height;
|
||||
mat4 transform = (mat4){{
|
||||
xscale, 0.0, 0.0, 0.0,
|
||||
0.0, yscale, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}};
|
||||
return transform;
|
||||
}
|
||||
|
||||
mat4 get_fit_view_transform(float widget_aspect_ratio, float frame_aspect_ratio) {
|
||||
float zx = 1, zy = 1;
|
||||
if (frame_aspect_ratio > widget_aspect_ratio) {
|
||||
zy = widget_aspect_ratio / frame_aspect_ratio;
|
||||
} else {
|
||||
zx = frame_aspect_ratio / widget_aspect_ratio;
|
||||
}
|
||||
|
||||
const mat4 frame_transform = {{
|
||||
zx, 0.0, 0.0, 0.0,
|
||||
0.0, zy, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}};
|
||||
return frame_transform;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, bool zoom, QWidget* parent) :
|
||||
stream_name(stream_name), requested_stream_type(type), zoomed_view(zoom), QOpenGLWidget(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);
|
||||
}
|
||||
|
||||
CameraWidget::~CameraWidget() {
|
||||
makeCurrent();
|
||||
stopVipcThread();
|
||||
if (isValid()) {
|
||||
glDeleteVertexArrays(1, &frame_vao);
|
||||
glDeleteBuffers(1, &frame_vbo);
|
||||
glDeleteBuffers(1, &frame_ibo);
|
||||
glDeleteBuffers(2, textures);
|
||||
}
|
||||
doneCurrent();
|
||||
}
|
||||
|
||||
// Qt uses device-independent pixels, depending on platform this may be
|
||||
// different to what OpenGL uses
|
||||
int CameraWidget::glWidth() {
|
||||
return width() * devicePixelRatio();
|
||||
}
|
||||
|
||||
int CameraWidget::glHeight() {
|
||||
return height() * devicePixelRatio();
|
||||
}
|
||||
|
||||
void CameraWidget::initializeGL() {
|
||||
initializeOpenGLFunctions();
|
||||
|
||||
program = std::make_unique<QOpenGLShaderProgram>(context());
|
||||
bool ret = program->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader);
|
||||
assert(ret);
|
||||
ret = program->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader);
|
||||
assert(ret);
|
||||
|
||||
program->link();
|
||||
GLint frame_pos_loc = program->attributeLocation("aPosition");
|
||||
GLint frame_texcoord_loc = 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);
|
||||
|
||||
glUseProgram(program->programId());
|
||||
|
||||
#ifdef QCOM2
|
||||
glUniform1i(program->uniformLocation("uTexture"), 0);
|
||||
#else
|
||||
glGenTextures(2, textures);
|
||||
glUniform1i(program->uniformLocation("uTextureY"), 0);
|
||||
glUniform1i(program->uniformLocation("uTextureUV"), 1);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CameraWidget::showEvent(QShowEvent *event) {
|
||||
if (!vipc_thread) {
|
||||
clearFrames();
|
||||
vipc_thread = new QThread();
|
||||
connect(vipc_thread, &QThread::started, [=]() { vipcThread(); });
|
||||
connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater);
|
||||
vipc_thread->start();
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::stopVipcThread() {
|
||||
makeCurrent();
|
||||
if (vipc_thread) {
|
||||
vipc_thread->requestInterruption();
|
||||
vipc_thread->quit();
|
||||
vipc_thread->wait();
|
||||
vipc_thread = nullptr;
|
||||
}
|
||||
|
||||
#ifdef QCOM2
|
||||
EGLDisplay egl_display = eglGetCurrentDisplay();
|
||||
assert(egl_display != EGL_NO_DISPLAY);
|
||||
for (auto &pair : egl_images) {
|
||||
eglDestroyImageKHR(egl_display, pair.second);
|
||||
assert(eglGetError() == EGL_SUCCESS);
|
||||
}
|
||||
egl_images.clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
void CameraWidget::availableStreamsUpdated(std::set<VisionStreamType> streams) {
|
||||
available_streams = streams;
|
||||
}
|
||||
|
||||
void CameraWidget::updateFrameMat() {
|
||||
int w = glWidth(), h = glHeight();
|
||||
|
||||
if (zoomed_view) {
|
||||
if (active_stream_type == VISION_STREAM_DRIVER) {
|
||||
if (stream_width > 0 && stream_height > 0) {
|
||||
frame_mat = get_driver_view_transform(w, h, stream_width, stream_height);
|
||||
}
|
||||
} else {
|
||||
// Project point at "infinity" to compute x and y offsets
|
||||
// to ensure this ends up in the middle of the screen
|
||||
// for narrow come and a little lower for wide cam.
|
||||
// TODO: use proper perspective transform?
|
||||
if (active_stream_type == VISION_STREAM_WIDE_ROAD) {
|
||||
intrinsic_matrix = ECAM_INTRINSIC_MATRIX;
|
||||
zoom = 2.0;
|
||||
} else {
|
||||
intrinsic_matrix = FCAM_INTRINSIC_MATRIX;
|
||||
zoom = 1.1;
|
||||
}
|
||||
const vec3 inf = {{1000., 0., 0.}};
|
||||
const vec3 Ep = matvecmul3(calibration, inf);
|
||||
const vec3 Kep = matvecmul3(intrinsic_matrix, Ep);
|
||||
|
||||
float x_offset_ = (Kep.v[0] / Kep.v[2] - intrinsic_matrix.v[2]) * zoom;
|
||||
float y_offset_ = (Kep.v[1] / Kep.v[2] - intrinsic_matrix.v[5]) * zoom;
|
||||
|
||||
float max_x_offset = intrinsic_matrix.v[2] * zoom - w / 2 - 5;
|
||||
float max_y_offset = intrinsic_matrix.v[5] * zoom - h / 2 - 5;
|
||||
|
||||
x_offset = std::clamp(x_offset_, -max_x_offset, max_x_offset);
|
||||
y_offset = std::clamp(y_offset_, -max_y_offset, max_y_offset);
|
||||
|
||||
float zx = zoom * 2 * intrinsic_matrix.v[2] / w;
|
||||
float zy = zoom * 2 * intrinsic_matrix.v[5] / h;
|
||||
const mat4 frame_transform = {{
|
||||
zx, 0.0, 0.0, -x_offset / w * 2,
|
||||
0.0, zy, 0.0, y_offset / h * 2,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,
|
||||
}};
|
||||
frame_mat = frame_transform;
|
||||
}
|
||||
} else if (stream_width > 0 && stream_height > 0) {
|
||||
// fit frame to widget size
|
||||
float widget_aspect_ratio = (float)w / h;
|
||||
float frame_aspect_ratio = (float)stream_width / stream_height;
|
||||
frame_mat = get_fit_view_transform(widget_aspect_ratio, frame_aspect_ratio);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::updateCalibration(const mat3 &calib) {
|
||||
calibration = calib;
|
||||
}
|
||||
|
||||
void CameraWidget::paintGL() {
|
||||
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF());
|
||||
glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
|
||||
|
||||
std::lock_guard lk(frame_lock);
|
||||
if (frames.empty()) return;
|
||||
|
||||
int frame_idx = frames.size() - 1;
|
||||
|
||||
// Always draw latest frame until sync logic is more stable
|
||||
// for (frame_idx = 0; frame_idx < frames.size() - 1; frame_idx++) {
|
||||
// if (frames[frame_idx].first == draw_frame_id) break;
|
||||
// }
|
||||
|
||||
// Log duplicate/dropped frames
|
||||
if (frames[frame_idx].first == prev_frame_id) {
|
||||
qDebug() << "Drawing same frame twice" << frames[frame_idx].first;
|
||||
} else if (frames[frame_idx].first != prev_frame_id + 1) {
|
||||
qDebug() << "Skipped frame" << frames[frame_idx].first;
|
||||
}
|
||||
prev_frame_id = frames[frame_idx].first;
|
||||
VisionBuf *frame = frames[frame_idx].second;
|
||||
assert(frame != nullptr);
|
||||
|
||||
updateFrameMat();
|
||||
|
||||
glViewport(0, 0, glWidth(), glHeight());
|
||||
glBindVertexArray(frame_vao);
|
||||
glUseProgram(program->programId());
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
|
||||
#ifdef QCOM2
|
||||
// no frame copy
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, egl_images[frame->idx]);
|
||||
assert(glGetError() == GL_NO_ERROR);
|
||||
#else
|
||||
// fallback to copy
|
||||
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, frame->y);
|
||||
assert(glGetError() == GL_NO_ERROR);
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2);
|
||||
glActiveTexture(GL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, textures[1]);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, frame->uv);
|
||||
assert(glGetError() == GL_NO_ERROR);
|
||||
#endif
|
||||
|
||||
glUniformMatrix4fv(program->uniformLocation("uTransform"), 1, GL_TRUE, frame_mat.v);
|
||||
glEnableVertexAttribArray(0);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (const void *)0);
|
||||
glDisableVertexAttribArray(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
#ifdef QCOM2
|
||||
EGLDisplay egl_display = eglGetCurrentDisplay();
|
||||
assert(egl_display != EGL_NO_DISPLAY);
|
||||
for (auto &pair : egl_images) {
|
||||
eglDestroyImageKHR(egl_display, pair.second);
|
||||
}
|
||||
egl_images.clear();
|
||||
|
||||
for (int i = 0; i < vipc_client->num_buffers; i++) { // import buffers into OpenGL
|
||||
int fd = dup(vipc_client->buffers[i].fd); // eglDestroyImageKHR will close, so duplicate
|
||||
EGLint img_attrs[] = {
|
||||
EGL_WIDTH, (int)vipc_client->buffers[i].width,
|
||||
EGL_HEIGHT, (int)vipc_client->buffers[i].height,
|
||||
EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_NV12,
|
||||
EGL_DMA_BUF_PLANE0_FD_EXT, fd,
|
||||
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
|
||||
EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)vipc_client->buffers[i].stride,
|
||||
EGL_DMA_BUF_PLANE1_FD_EXT, fd,
|
||||
EGL_DMA_BUF_PLANE1_OFFSET_EXT, (int)vipc_client->buffers[i].uv_offset,
|
||||
EGL_DMA_BUF_PLANE1_PITCH_EXT, (int)vipc_client->buffers[i].stride,
|
||||
EGL_NONE
|
||||
};
|
||||
egl_images[i] = eglCreateImageKHR(egl_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, img_attrs);
|
||||
assert(eglGetError() == EGL_SUCCESS);
|
||||
}
|
||||
#else
|
||||
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);
|
||||
#endif
|
||||
}
|
||||
|
||||
void CameraWidget::vipcFrameReceived() {
|
||||
update();
|
||||
}
|
||||
|
||||
void CameraWidget::vipcThread() {
|
||||
VisionStreamType cur_stream = requested_stream_type;
|
||||
std::unique_ptr<VisionIpcClient> vipc_client;
|
||||
VisionIpcBufExtra meta_main = {0};
|
||||
|
||||
while (!QThread::currentThread()->isInterruptionRequested()) {
|
||||
if (!vipc_client || cur_stream != requested_stream_type) {
|
||||
clearFrames();
|
||||
qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream;
|
||||
cur_stream = requested_stream_type;
|
||||
vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false));
|
||||
}
|
||||
active_stream_type = cur_stream;
|
||||
|
||||
if (!vipc_client->connected) {
|
||||
clearFrames();
|
||||
auto streams = VisionIpcClient::getAvailableStreams(stream_name, false);
|
||||
if (streams.empty()) {
|
||||
QThread::msleep(100);
|
||||
continue;
|
||||
}
|
||||
emit vipcAvailableStreamsUpdated(streams);
|
||||
|
||||
if (!vipc_client->connect(false)) {
|
||||
QThread::msleep(100);
|
||||
continue;
|
||||
}
|
||||
emit vipcThreadConnected(vipc_client.get());
|
||||
}
|
||||
|
||||
if (VisionBuf *buf = vipc_client->recv(&meta_main, 1000)) {
|
||||
{
|
||||
std::lock_guard lk(frame_lock);
|
||||
frames.push_back(std::make_pair(meta_main.frame_id, buf));
|
||||
while (frames.size() > FRAME_BUFFER_SIZE) {
|
||||
frames.pop_front();
|
||||
}
|
||||
}
|
||||
emit vipcThreadFrameReceived();
|
||||
} else {
|
||||
if (!isVisible()) {
|
||||
vipc_client->connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CameraWidget::clearFrames() {
|
||||
std::lock_guard lk(frame_lock);
|
||||
frames.clear();
|
||||
available_streams.clear();
|
||||
}
|
||||
103
selfdrive/ui/qt/widgets/cameraview.h
Normal file
103
selfdrive/ui/qt/widgets/cameraview.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShaderProgram>
|
||||
#include <QOpenGLWidget>
|
||||
#include <QThread>
|
||||
|
||||
#ifdef QCOM2
|
||||
#define EGL_EGLEXT_PROTOTYPES
|
||||
#define EGL_NO_X11
|
||||
#define GL_TEXTURE_EXTERNAL_OES 0x8D65
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <drm/drm_fourcc.h>
|
||||
#endif
|
||||
|
||||
#include "cereal/visionipc/visionipc_client.h"
|
||||
#include "system/camerad/cameras/camera_common.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
const int FRAME_BUFFER_SIZE = 5;
|
||||
static_assert(FRAME_BUFFER_SIZE <= YUV_BUFFER_COUNT);
|
||||
|
||||
class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using QOpenGLWidget::QOpenGLWidget;
|
||||
explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, bool zoom, QWidget* parent = nullptr);
|
||||
~CameraWidget();
|
||||
void setBackgroundColor(const QColor &color) { bg = color; }
|
||||
void setFrameId(int frame_id) { draw_frame_id = frame_id; }
|
||||
void setStreamType(VisionStreamType type) { requested_stream_type = type; }
|
||||
VisionStreamType getStreamType() { return active_stream_type; }
|
||||
void stopVipcThread();
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
void vipcThreadConnected(VisionIpcClient *);
|
||||
void vipcThreadFrameReceived();
|
||||
void vipcAvailableStreamsUpdated(std::set<VisionStreamType>);
|
||||
|
||||
protected:
|
||||
void paintGL() override;
|
||||
void initializeGL() override;
|
||||
void resizeGL(int w, int h) override { updateFrameMat(); }
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); }
|
||||
virtual void updateFrameMat();
|
||||
void updateCalibration(const mat3 &calib);
|
||||
void vipcThread();
|
||||
void clearFrames();
|
||||
|
||||
int glWidth();
|
||||
int glHeight();
|
||||
|
||||
bool zoomed_view;
|
||||
GLuint frame_vao, frame_vbo, frame_ibo;
|
||||
GLuint textures[2];
|
||||
mat4 frame_mat = {};
|
||||
std::unique_ptr<QOpenGLShaderProgram> program;
|
||||
QColor bg = QColor("#000000");
|
||||
|
||||
#ifdef QCOM2
|
||||
std::map<int, EGLImageKHR> egl_images;
|
||||
#endif
|
||||
|
||||
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;
|
||||
|
||||
// Calibration
|
||||
float x_offset = 0;
|
||||
float y_offset = 0;
|
||||
float zoom = 1.0;
|
||||
mat3 calibration = DEFAULT_CALIBRATION;
|
||||
mat3 intrinsic_matrix = FCAM_INTRINSIC_MATRIX;
|
||||
|
||||
std::recursive_mutex frame_lock;
|
||||
std::deque<std::pair<uint32_t, VisionBuf*>> frames;
|
||||
uint32_t draw_frame_id = 0;
|
||||
uint32_t prev_frame_id = 0;
|
||||
|
||||
protected slots:
|
||||
void vipcConnected(VisionIpcClient *vipc_client);
|
||||
void vipcFrameReceived();
|
||||
void availableStreamsUpdated(std::set<VisionStreamType> streams);
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(std::set<VisionStreamType>);
|
||||
141
selfdrive/ui/qt/widgets/controls.cc
Normal file
141
selfdrive/ui/qt/widgets/controls.cc
Normal file
@@ -0,0 +1,141 @@
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QStyleOption>
|
||||
|
||||
AbstractControl::AbstractControl(const QString &title, const QString &desc, const QString &icon, QWidget *parent) : QFrame(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(0);
|
||||
|
||||
hlayout = new QHBoxLayout;
|
||||
hlayout->setMargin(0);
|
||||
hlayout->setSpacing(20);
|
||||
|
||||
// left icon
|
||||
icon_label = new QLabel(this);
|
||||
hlayout->addWidget(icon_label);
|
||||
if (!icon.isEmpty()) {
|
||||
icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation);
|
||||
icon_label->setPixmap(icon_pixmap);
|
||||
icon_label->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
}
|
||||
icon_label->setVisible(!icon.isEmpty());
|
||||
|
||||
// title
|
||||
title_label = new QPushButton(title);
|
||||
title_label->setFixedHeight(120);
|
||||
title_label->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; border: none;");
|
||||
hlayout->addWidget(title_label, 1);
|
||||
|
||||
// value next to control button
|
||||
value = new ElidedLabel();
|
||||
value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
value->setStyleSheet("color: #aaaaaa");
|
||||
hlayout->addWidget(value);
|
||||
|
||||
main_layout->addLayout(hlayout);
|
||||
|
||||
// description
|
||||
description = new QLabel(desc);
|
||||
description->setContentsMargins(40, 20, 40, 20);
|
||||
description->setStyleSheet("font-size: 40px; color: grey");
|
||||
description->setWordWrap(true);
|
||||
description->setVisible(false);
|
||||
main_layout->addWidget(description);
|
||||
|
||||
connect(title_label, &QPushButton::clicked, [=]() {
|
||||
if (!description->isVisible()) {
|
||||
emit showDescriptionEvent();
|
||||
}
|
||||
|
||||
if (!description->text().isEmpty()) {
|
||||
description->setVisible(!description->isVisible());
|
||||
}
|
||||
});
|
||||
|
||||
main_layout->addStretch();
|
||||
}
|
||||
|
||||
void AbstractControl::hideEvent(QHideEvent *e) {
|
||||
if (description != nullptr) {
|
||||
description->hide();
|
||||
}
|
||||
}
|
||||
|
||||
// controls
|
||||
|
||||
ButtonControl::ButtonControl(const QString &title, const QString &text, const QString &desc, QWidget *parent) : AbstractControl(title, desc, "", parent) {
|
||||
btn.setText(text);
|
||||
btn.setStyleSheet(R"(
|
||||
QPushButton {
|
||||
padding: 0;
|
||||
border-radius: 50px;
|
||||
font-size: 35px;
|
||||
font-weight: 500;
|
||||
color: #E4E4E4;
|
||||
background-color: #393939;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
color: #33E4E4E4;
|
||||
}
|
||||
)");
|
||||
btn.setFixedSize(250, 100);
|
||||
QObject::connect(&btn, &QPushButton::clicked, this, &ButtonControl::clicked);
|
||||
hlayout->addWidget(&btn);
|
||||
}
|
||||
|
||||
// ElidedLabel
|
||||
|
||||
ElidedLabel::ElidedLabel(QWidget *parent) : ElidedLabel({}, parent) {}
|
||||
|
||||
ElidedLabel::ElidedLabel(const QString &text, QWidget *parent) : QLabel(text.trimmed(), parent) {
|
||||
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
|
||||
setMinimumWidth(1);
|
||||
}
|
||||
|
||||
void ElidedLabel::resizeEvent(QResizeEvent* event) {
|
||||
QLabel::resizeEvent(event);
|
||||
lastText_ = elidedText_ = "";
|
||||
}
|
||||
|
||||
void ElidedLabel::paintEvent(QPaintEvent *event) {
|
||||
const QString curText = text();
|
||||
if (curText != lastText_) {
|
||||
elidedText_ = fontMetrics().elidedText(curText, Qt::ElideRight, contentsRect().width());
|
||||
lastText_ = curText;
|
||||
}
|
||||
|
||||
QPainter painter(this);
|
||||
drawFrame(&painter);
|
||||
QStyleOption opt;
|
||||
opt.initFrom(this);
|
||||
style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole());
|
||||
}
|
||||
|
||||
// ParamControl
|
||||
|
||||
ParamControl::ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent)
|
||||
: ToggleControl(title, desc, icon, false, parent) {
|
||||
key = param.toStdString();
|
||||
QObject::connect(this, &ParamControl::toggleFlipped, this, &ParamControl::toggleClicked);
|
||||
}
|
||||
|
||||
void ParamControl::toggleClicked(bool state) {
|
||||
auto do_confirm = [this]() {
|
||||
QString content("<body><h2 style=\"text-align: center;\">" + title_label->text() + "</h2><br>"
|
||||
"<p style=\"text-align: center; margin: 0 128px; font-size: 50px;\">" + getDescription() + "</p></body>");
|
||||
return ConfirmationDialog(content, tr("Enable"), tr("Cancel"), true, this).exec();
|
||||
};
|
||||
|
||||
bool confirmed = store_confirm && params.getBool(key + "Confirmed");
|
||||
if (!confirm || confirmed || !state || do_confirm()) {
|
||||
if (store_confirm && state) params.putBool(key + "Confirmed", true);
|
||||
params.putBool(key, state);
|
||||
setIcon(state);
|
||||
} else {
|
||||
toggle.togglePosition();
|
||||
}
|
||||
}
|
||||
285
selfdrive/ui/qt/widgets/controls.h
Normal file
285
selfdrive/ui/qt/widgets/controls.h
Normal file
@@ -0,0 +1,285 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
#include "selfdrive/ui/qt/widgets/toggle.h"
|
||||
|
||||
class ElidedLabel : public QLabel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ElidedLabel(QWidget *parent = 0);
|
||||
explicit ElidedLabel(const QString &text, QWidget *parent = 0);
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override {
|
||||
if (rect().contains(event->pos())) {
|
||||
emit clicked();
|
||||
}
|
||||
}
|
||||
QString lastText_, elidedText_;
|
||||
};
|
||||
|
||||
|
||||
class AbstractControl : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
void setDescription(const QString &desc) {
|
||||
if (description) description->setText(desc);
|
||||
}
|
||||
|
||||
void setTitle(const QString &title) {
|
||||
title_label->setText(title);
|
||||
}
|
||||
|
||||
void setValue(const QString &val) {
|
||||
value->setText(val);
|
||||
}
|
||||
|
||||
const QString getDescription() {
|
||||
return description->text();
|
||||
}
|
||||
|
||||
QLabel *icon_label;
|
||||
QPixmap icon_pixmap;
|
||||
|
||||
public slots:
|
||||
void showDescription() {
|
||||
description->setVisible(true);
|
||||
}
|
||||
|
||||
signals:
|
||||
void showDescriptionEvent();
|
||||
|
||||
protected:
|
||||
AbstractControl(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr);
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
|
||||
QHBoxLayout *hlayout;
|
||||
QPushButton *title_label;
|
||||
|
||||
private:
|
||||
ElidedLabel *value;
|
||||
QLabel *description = nullptr;
|
||||
};
|
||||
|
||||
// widget to display a value
|
||||
class LabelControl : public AbstractControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LabelControl(const QString &title, const QString &text = "", const QString &desc = "", QWidget *parent = nullptr) : AbstractControl(title, desc, "", parent) {
|
||||
label.setText(text);
|
||||
label.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
hlayout->addWidget(&label);
|
||||
}
|
||||
void setText(const QString &text) { label.setText(text); }
|
||||
|
||||
private:
|
||||
ElidedLabel label;
|
||||
};
|
||||
|
||||
// widget for a button with a label
|
||||
class ButtonControl : public AbstractControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ButtonControl(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr);
|
||||
inline void setText(const QString &text) { btn.setText(text); }
|
||||
inline QString text() const { return btn.text(); }
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
public slots:
|
||||
void setEnabled(bool enabled) { btn.setEnabled(enabled); }
|
||||
|
||||
private:
|
||||
QPushButton btn;
|
||||
};
|
||||
|
||||
class ToggleControl : public AbstractControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ToggleControl(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControl(title, desc, icon, parent) {
|
||||
toggle.setFixedSize(150, 100);
|
||||
if (state) {
|
||||
toggle.togglePosition();
|
||||
}
|
||||
hlayout->addWidget(&toggle);
|
||||
QObject::connect(&toggle, &Toggle::stateChanged, this, &ToggleControl::toggleFlipped);
|
||||
}
|
||||
|
||||
void setEnabled(bool enabled) {
|
||||
toggle.setEnabled(enabled);
|
||||
toggle.update();
|
||||
}
|
||||
|
||||
signals:
|
||||
void toggleFlipped(bool state);
|
||||
|
||||
protected:
|
||||
Toggle toggle;
|
||||
};
|
||||
|
||||
// widget to toggle params
|
||||
class ParamControl : public ToggleControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr);
|
||||
void setConfirmation(bool _confirm, bool _store_confirm) {
|
||||
confirm = _confirm;
|
||||
store_confirm = _store_confirm;
|
||||
}
|
||||
|
||||
void setActiveIcon(const QString &icon) {
|
||||
active_icon_pixmap = QPixmap(icon).scaledToWidth(80, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
bool state = params.getBool(key);
|
||||
if (state != toggle.on) {
|
||||
toggle.togglePosition();
|
||||
setIcon(state);
|
||||
}
|
||||
}
|
||||
|
||||
void showEvent(QShowEvent *event) override {
|
||||
refresh();
|
||||
}
|
||||
|
||||
private:
|
||||
void toggleClicked(bool state);
|
||||
void setIcon(bool state) {
|
||||
if (state && !active_icon_pixmap.isNull()) {
|
||||
icon_label->setPixmap(active_icon_pixmap);
|
||||
} else if (!icon_pixmap.isNull()) {
|
||||
icon_label->setPixmap(icon_pixmap);
|
||||
}
|
||||
}
|
||||
|
||||
std::string key;
|
||||
Params params;
|
||||
QPixmap active_icon_pixmap;
|
||||
bool confirm = false;
|
||||
bool store_confirm = false;
|
||||
};
|
||||
|
||||
class ButtonParamControl : public AbstractControl {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ButtonParamControl(const QString ¶m, const QString &title, const QString &desc, const QString &icon,
|
||||
const std::vector<QString> &button_texts, const int minimum_button_width = 225) : AbstractControl(title, desc, icon) {
|
||||
const QString style = R"(
|
||||
QPushButton {
|
||||
border-radius: 50px;
|
||||
font-size: 40px;
|
||||
font-weight: 500;
|
||||
height:100px;
|
||||
padding: 0 25 0 25;
|
||||
color: #E4E4E4;
|
||||
background-color: #393939;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
QPushButton:checked:enabled {
|
||||
background-color: #33Ab4C;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
color: #33E4E4E4;
|
||||
}
|
||||
)";
|
||||
key = param.toStdString();
|
||||
int value = atoi(params.get(key).c_str());
|
||||
|
||||
button_group = new QButtonGroup(this);
|
||||
button_group->setExclusive(true);
|
||||
for (int i = 0; i < button_texts.size(); i++) {
|
||||
QPushButton *button = new QPushButton(button_texts[i], this);
|
||||
button->setCheckable(true);
|
||||
button->setChecked(i == value);
|
||||
button->setStyleSheet(style);
|
||||
button->setMinimumWidth(minimum_button_width);
|
||||
hlayout->addWidget(button);
|
||||
button_group->addButton(button, i);
|
||||
}
|
||||
|
||||
QObject::connect(button_group, QOverload<int, bool>::of(&QButtonGroup::buttonToggled), [=](int id, bool checked) {
|
||||
if (checked) {
|
||||
params.put(key, std::to_string(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void setEnabled(bool enable) {
|
||||
for (auto btn : button_group->buttons()) {
|
||||
btn->setEnabled(enable);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::string key;
|
||||
Params params;
|
||||
QButtonGroup *button_group;
|
||||
};
|
||||
|
||||
class ListWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ListWidget(QWidget *parent = 0) : QWidget(parent), outer_layout(this) {
|
||||
outer_layout.setMargin(0);
|
||||
outer_layout.setSpacing(0);
|
||||
outer_layout.addLayout(&inner_layout);
|
||||
inner_layout.setMargin(0);
|
||||
inner_layout.setSpacing(25); // default spacing is 25
|
||||
outer_layout.addStretch();
|
||||
}
|
||||
inline void addItem(QWidget *w) { inner_layout.addWidget(w); }
|
||||
inline void addItem(QLayout *layout) { inner_layout.addLayout(layout); }
|
||||
inline void setSpacing(int spacing) { inner_layout.setSpacing(spacing); }
|
||||
|
||||
private:
|
||||
void paintEvent(QPaintEvent *) override {
|
||||
QPainter p(this);
|
||||
p.setPen(Qt::gray);
|
||||
for (int i = 0; i < inner_layout.count() - 1; ++i) {
|
||||
QWidget *widget = inner_layout.itemAt(i)->widget();
|
||||
if (widget == nullptr || widget->isVisible()) {
|
||||
QRect r = inner_layout.itemAt(i)->geometry();
|
||||
int bottom = r.bottom() + inner_layout.spacing() / 2;
|
||||
p.drawLine(r.left() + 40, bottom, r.right() - 40, bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
QVBoxLayout outer_layout;
|
||||
QVBoxLayout inner_layout;
|
||||
};
|
||||
|
||||
// convenience class for wrapping layouts
|
||||
class LayoutWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
LayoutWidget(QLayout *l, QWidget *parent = nullptr) : QWidget(parent) {
|
||||
setLayout(l);
|
||||
}
|
||||
};
|
||||
336
selfdrive/ui/qt/widgets/input.cc
Normal file
336
selfdrive/ui/qt/widgets/input.cc
Normal file
@@ -0,0 +1,336 @@
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
|
||||
#include <QPushButton>
|
||||
#include <QButtonGroup>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
|
||||
DialogBase::DialogBase(QWidget *parent) : QDialog(parent) {
|
||||
Q_ASSERT(parent != nullptr);
|
||||
parent->installEventFilter(this);
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
outline: none;
|
||||
color: white;
|
||||
font-family: Inter;
|
||||
}
|
||||
DialogBase {
|
||||
background-color: black;
|
||||
}
|
||||
QPushButton {
|
||||
height: 160;
|
||||
font-size: 55px;
|
||||
font-weight: 400;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
background-color: #333333;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #444444;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
bool DialogBase::eventFilter(QObject *o, QEvent *e) {
|
||||
if (o == parent() && e->type() == QEvent::Hide) {
|
||||
reject();
|
||||
}
|
||||
return QDialog::eventFilter(o, e);
|
||||
}
|
||||
|
||||
int DialogBase::exec() {
|
||||
setMainWindow(this);
|
||||
return QDialog::exec();
|
||||
}
|
||||
|
||||
InputDialog::InputDialog(const QString &title, QWidget *parent, const QString &subtitle, bool secret) : DialogBase(parent) {
|
||||
main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(50, 55, 50, 50);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
// build header
|
||||
QHBoxLayout *header_layout = new QHBoxLayout();
|
||||
|
||||
QVBoxLayout *vlayout = new QVBoxLayout;
|
||||
header_layout->addLayout(vlayout);
|
||||
label = new QLabel(title, this);
|
||||
label->setStyleSheet("font-size: 90px; font-weight: bold;");
|
||||
vlayout->addWidget(label, 1, Qt::AlignTop | Qt::AlignLeft);
|
||||
|
||||
if (!subtitle.isEmpty()) {
|
||||
sublabel = new QLabel(subtitle, this);
|
||||
sublabel->setStyleSheet("font-size: 55px; font-weight: light; color: #BDBDBD;");
|
||||
vlayout->addWidget(sublabel, 1, Qt::AlignTop | Qt::AlignLeft);
|
||||
}
|
||||
|
||||
QPushButton* cancel_btn = new QPushButton(tr("Cancel"));
|
||||
cancel_btn->setFixedSize(386, 125);
|
||||
cancel_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 48px;
|
||||
border-radius: 10px;
|
||||
color: #E4E4E4;
|
||||
background-color: #333333;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #444444;
|
||||
}
|
||||
)");
|
||||
header_layout->addWidget(cancel_btn, 0, Qt::AlignRight);
|
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::reject);
|
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &InputDialog::cancel);
|
||||
|
||||
main_layout->addLayout(header_layout);
|
||||
|
||||
// text box
|
||||
main_layout->addStretch(2);
|
||||
|
||||
QWidget *textbox_widget = new QWidget;
|
||||
textbox_widget->setObjectName("textbox");
|
||||
QHBoxLayout *textbox_layout = new QHBoxLayout(textbox_widget);
|
||||
textbox_layout->setContentsMargins(50, 0, 50, 0);
|
||||
|
||||
textbox_widget->setStyleSheet(R"(
|
||||
#textbox {
|
||||
margin-left: 50px;
|
||||
margin-right: 50px;
|
||||
border-radius: 0;
|
||||
border-bottom: 3px solid #BDBDBD;
|
||||
}
|
||||
* {
|
||||
border: none;
|
||||
font-size: 80px;
|
||||
font-weight: light;
|
||||
background-color: transparent;
|
||||
}
|
||||
)");
|
||||
|
||||
line = new QLineEdit();
|
||||
line->setStyleSheet("lineedit-password-character: 8226; lineedit-password-mask-delay: 1500;");
|
||||
textbox_layout->addWidget(line, 1);
|
||||
|
||||
if (secret) {
|
||||
eye_btn = new QPushButton();
|
||||
eye_btn->setCheckable(true);
|
||||
eye_btn->setFixedSize(150, 120);
|
||||
QObject::connect(eye_btn, &QPushButton::toggled, [=](bool checked) {
|
||||
if (checked) {
|
||||
eye_btn->setIcon(QIcon(ASSET_PATH + "img_eye_closed.svg"));
|
||||
eye_btn->setIconSize(QSize(81, 54));
|
||||
line->setEchoMode(QLineEdit::Password);
|
||||
} else {
|
||||
eye_btn->setIcon(QIcon(ASSET_PATH + "img_eye_open.svg"));
|
||||
eye_btn->setIconSize(QSize(81, 44));
|
||||
line->setEchoMode(QLineEdit::Normal);
|
||||
}
|
||||
});
|
||||
eye_btn->toggle();
|
||||
eye_btn->setChecked(false);
|
||||
textbox_layout->addWidget(eye_btn);
|
||||
}
|
||||
|
||||
main_layout->addWidget(textbox_widget, 0, Qt::AlignBottom);
|
||||
main_layout->addSpacing(25);
|
||||
|
||||
k = new Keyboard(this);
|
||||
QObject::connect(k, &Keyboard::emitEnter, this, &InputDialog::handleEnter);
|
||||
QObject::connect(k, &Keyboard::emitBackspace, this, [=]() {
|
||||
line->backspace();
|
||||
});
|
||||
QObject::connect(k, &Keyboard::emitKey, this, [=](const QString &key) {
|
||||
line->insert(key.left(1));
|
||||
});
|
||||
|
||||
main_layout->addWidget(k, 2, Qt::AlignBottom);
|
||||
}
|
||||
|
||||
QString InputDialog::getText(const QString &prompt, QWidget *parent, const QString &subtitle,
|
||||
bool secret, int minLength, const QString &defaultText) {
|
||||
InputDialog d = InputDialog(prompt, parent, subtitle, secret);
|
||||
d.line->setText(defaultText);
|
||||
d.setMinLength(minLength);
|
||||
const int ret = d.exec();
|
||||
return ret ? d.text() : QString();
|
||||
}
|
||||
|
||||
QString InputDialog::text() {
|
||||
return line->text();
|
||||
}
|
||||
|
||||
void InputDialog::show() {
|
||||
setMainWindow(this);
|
||||
}
|
||||
|
||||
void InputDialog::handleEnter() {
|
||||
if (line->text().length() >= minLength) {
|
||||
done(QDialog::Accepted);
|
||||
emitText(line->text());
|
||||
} else {
|
||||
setMessage(tr("Need at least %n character(s)!", "", minLength), false);
|
||||
}
|
||||
}
|
||||
|
||||
void InputDialog::setMessage(const QString &message, bool clearInputField) {
|
||||
label->setText(message);
|
||||
if (clearInputField) {
|
||||
line->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
void InputDialog::setMinLength(int length) {
|
||||
minLength = length;
|
||||
}
|
||||
|
||||
// ConfirmationDialog
|
||||
|
||||
ConfirmationDialog::ConfirmationDialog(const QString &prompt_text, const QString &confirm_text, const QString &cancel_text,
|
||||
const bool rich, QWidget *parent) : DialogBase(parent) {
|
||||
QFrame *container = new QFrame(this);
|
||||
container->setStyleSheet(R"(
|
||||
QFrame { background-color: #1B1B1B; color: #C9C9C9; }
|
||||
#confirm_btn { background-color: #465BEA; }
|
||||
#confirm_btn:pressed { background-color: #3049F4; }
|
||||
)");
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(container);
|
||||
main_layout->setContentsMargins(32, rich ? 32 : 120, 32, 32);
|
||||
|
||||
QLabel *prompt = new QLabel(prompt_text, this);
|
||||
prompt->setWordWrap(true);
|
||||
prompt->setAlignment(rich ? Qt::AlignLeft : Qt::AlignHCenter);
|
||||
prompt->setStyleSheet((rich ? "font-size: 42px; font-weight: light;" : "font-size: 70px; font-weight: bold;") + QString(" margin: 45px;"));
|
||||
main_layout->addWidget(rich ? (QWidget*)new ScrollView(prompt, this) : (QWidget*)prompt, 1, Qt::AlignTop);
|
||||
|
||||
// cancel + confirm buttons
|
||||
QHBoxLayout *btn_layout = new QHBoxLayout();
|
||||
btn_layout->setSpacing(30);
|
||||
main_layout->addLayout(btn_layout);
|
||||
|
||||
if (cancel_text.length()) {
|
||||
QPushButton* cancel_btn = new QPushButton(cancel_text);
|
||||
btn_layout->addWidget(cancel_btn);
|
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject);
|
||||
}
|
||||
|
||||
if (confirm_text.length()) {
|
||||
QPushButton* confirm_btn = new QPushButton(confirm_text);
|
||||
confirm_btn->setObjectName("confirm_btn");
|
||||
btn_layout->addWidget(confirm_btn);
|
||||
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept);
|
||||
}
|
||||
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this);
|
||||
int margin = rich ? 100 : 200;
|
||||
outer_layout->setContentsMargins(margin, margin, margin, margin);
|
||||
outer_layout->addWidget(container);
|
||||
}
|
||||
|
||||
bool ConfirmationDialog::alert(const QString &prompt_text, QWidget *parent) {
|
||||
ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", false, parent);
|
||||
return d.exec();
|
||||
}
|
||||
|
||||
bool ConfirmationDialog::confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent) {
|
||||
ConfirmationDialog d = ConfirmationDialog(prompt_text, confirm_text, tr("Cancel"), false, parent);
|
||||
return d.exec();
|
||||
}
|
||||
|
||||
bool ConfirmationDialog::rich(const QString &prompt_text, QWidget *parent) {
|
||||
ConfirmationDialog d = ConfirmationDialog(prompt_text, tr("Ok"), "", true, parent);
|
||||
return d.exec();
|
||||
}
|
||||
|
||||
// MultiOptionDialog
|
||||
|
||||
MultiOptionDialog::MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) : DialogBase(parent) {
|
||||
QFrame *container = new QFrame(this);
|
||||
container->setStyleSheet(R"(
|
||||
QFrame { background-color: #1B1B1B; }
|
||||
#confirm_btn[enabled="false"] { background-color: #2B2B2B; }
|
||||
#confirm_btn:enabled { background-color: #465BEA; }
|
||||
#confirm_btn:enabled:pressed { background-color: #3049F4; }
|
||||
)");
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(container);
|
||||
main_layout->setContentsMargins(55, 50, 55, 50);
|
||||
|
||||
QLabel *title = new QLabel(prompt_text, this);
|
||||
title->setStyleSheet("font-size: 70px; font-weight: 500;");
|
||||
main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop);
|
||||
main_layout->addSpacing(25);
|
||||
|
||||
QWidget *listWidget = new QWidget(this);
|
||||
QVBoxLayout *listLayout = new QVBoxLayout(listWidget);
|
||||
listLayout->setSpacing(20);
|
||||
listWidget->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
height: 135;
|
||||
padding: 0px 50px;
|
||||
text-align: left;
|
||||
font-size: 55px;
|
||||
font-weight: 300;
|
||||
border-radius: 10px;
|
||||
background-color: #4F4F4F;
|
||||
}
|
||||
QPushButton:checked { background-color: #465BEA; }
|
||||
)");
|
||||
|
||||
QButtonGroup *group = new QButtonGroup(listWidget);
|
||||
group->setExclusive(true);
|
||||
|
||||
QPushButton *confirm_btn = new QPushButton(tr("Select"));
|
||||
confirm_btn->setObjectName("confirm_btn");
|
||||
confirm_btn->setEnabled(false);
|
||||
|
||||
for (const QString &s : l) {
|
||||
QPushButton *selectionLabel = new QPushButton(s);
|
||||
selectionLabel->setCheckable(true);
|
||||
selectionLabel->setChecked(s == current);
|
||||
QObject::connect(selectionLabel, &QPushButton::toggled, [=](bool checked) {
|
||||
if (checked) selection = s;
|
||||
if (selection != current) {
|
||||
confirm_btn->setEnabled(true);
|
||||
} else {
|
||||
confirm_btn->setEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
group->addButton(selectionLabel);
|
||||
listLayout->addWidget(selectionLabel);
|
||||
}
|
||||
// add stretch to keep buttons spaced correctly
|
||||
listLayout->addStretch(1);
|
||||
|
||||
ScrollView *scroll_view = new ScrollView(listWidget, this);
|
||||
scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
|
||||
main_layout->addWidget(scroll_view);
|
||||
main_layout->addSpacing(35);
|
||||
|
||||
// cancel + confirm buttons
|
||||
QHBoxLayout *blayout = new QHBoxLayout;
|
||||
main_layout->addLayout(blayout);
|
||||
blayout->setSpacing(50);
|
||||
|
||||
QPushButton *cancel_btn = new QPushButton(tr("Cancel"));
|
||||
QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject);
|
||||
QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept);
|
||||
blayout->addWidget(cancel_btn);
|
||||
blayout->addWidget(confirm_btn);
|
||||
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this);
|
||||
outer_layout->setContentsMargins(50, 50, 50, 50);
|
||||
outer_layout->addWidget(container);
|
||||
}
|
||||
|
||||
QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent) {
|
||||
MultiOptionDialog d = MultiOptionDialog(prompt_text, l, current, parent);
|
||||
if (d.exec()) {
|
||||
return d.selection;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
71
selfdrive/ui/qt/widgets/input.h
Normal file
71
selfdrive/ui/qt/widgets/input.h
Normal file
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/keyboard.h"
|
||||
|
||||
|
||||
class DialogBase : public QDialog {
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
DialogBase(QWidget *parent);
|
||||
bool eventFilter(QObject *o, QEvent *e) override;
|
||||
|
||||
public slots:
|
||||
int exec() override;
|
||||
};
|
||||
|
||||
class InputDialog : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit InputDialog(const QString &title, QWidget *parent, const QString &subtitle = "", bool secret = false);
|
||||
static QString getText(const QString &title, QWidget *parent, const QString &subtitle = "",
|
||||
bool secret = false, int minLength = -1, const QString &defaultText = "");
|
||||
QString text();
|
||||
void setMessage(const QString &message, bool clearInputField = true);
|
||||
void setMinLength(int length);
|
||||
void show();
|
||||
|
||||
private:
|
||||
int minLength;
|
||||
QLineEdit *line;
|
||||
Keyboard *k;
|
||||
QLabel *label;
|
||||
QLabel *sublabel;
|
||||
QVBoxLayout *main_layout;
|
||||
QPushButton *eye_btn;
|
||||
|
||||
private slots:
|
||||
void handleEnter();
|
||||
|
||||
signals:
|
||||
void cancel();
|
||||
void emitText(const QString &text);
|
||||
};
|
||||
|
||||
class ConfirmationDialog : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfirmationDialog(const QString &prompt_text, const QString &confirm_text,
|
||||
const QString &cancel_text, const bool rich, QWidget* parent);
|
||||
static bool alert(const QString &prompt_text, QWidget *parent);
|
||||
static bool confirm(const QString &prompt_text, const QString &confirm_text, QWidget *parent);
|
||||
static bool rich(const QString &prompt_text, QWidget *parent);
|
||||
};
|
||||
|
||||
class MultiOptionDialog : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MultiOptionDialog(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent);
|
||||
static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent);
|
||||
QString selection;
|
||||
};
|
||||
167
selfdrive/ui/qt/widgets/keyboard.cc
Normal file
167
selfdrive/ui/qt/widgets/keyboard.cc
Normal file
@@ -0,0 +1,167 @@
|
||||
#include "selfdrive/ui/qt/widgets/keyboard.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMap>
|
||||
#include <QTouchEvent>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
const QString BACKSPACE_KEY = "⌫";
|
||||
const QString ENTER_KEY = "→";
|
||||
|
||||
const QMap<QString, int> KEY_STRETCH = {{" ", 5}, {ENTER_KEY, 2}};
|
||||
|
||||
const QStringList CONTROL_BUTTONS = {"↑", "↓", "ABC", "#+=", "123", BACKSPACE_KEY, ENTER_KEY};
|
||||
|
||||
const float key_spacing_vertical = 20;
|
||||
const float key_spacing_horizontal = 15;
|
||||
|
||||
KeyButton::KeyButton(const QString &text, QWidget *parent) : QPushButton(text, parent) {
|
||||
setAttribute(Qt::WA_AcceptTouchEvents);
|
||||
setFocusPolicy(Qt::NoFocus);
|
||||
}
|
||||
|
||||
bool KeyButton::event(QEvent *event) {
|
||||
if (event->type() == QEvent::TouchBegin || event->type() == QEvent::TouchEnd) {
|
||||
QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
|
||||
if (!touchEvent->touchPoints().empty()) {
|
||||
const QEvent::Type mouseType = event->type() == QEvent::TouchBegin ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease;
|
||||
QMouseEvent mouseEvent(mouseType, touchEvent->touchPoints().front().pos(), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
|
||||
QPushButton::event(&mouseEvent);
|
||||
event->accept();
|
||||
parentWidget()->update();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QPushButton::event(event);
|
||||
}
|
||||
|
||||
KeyboardLayout::KeyboardLayout(QWidget* parent, const std::vector<QVector<QString>>& layout) : QWidget(parent) {
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(0);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
QButtonGroup* btn_group = new QButtonGroup(this);
|
||||
QObject::connect(btn_group, SIGNAL(buttonClicked(QAbstractButton*)), parent, SLOT(handleButton(QAbstractButton*)));
|
||||
|
||||
for (const auto &s : layout) {
|
||||
QHBoxLayout *hlayout = new QHBoxLayout;
|
||||
hlayout->setSpacing(0);
|
||||
|
||||
if (main_layout->count() == 1) {
|
||||
hlayout->addSpacing(90);
|
||||
}
|
||||
|
||||
for (const QString &p : s) {
|
||||
KeyButton* btn = new KeyButton(p);
|
||||
if (p == BACKSPACE_KEY) {
|
||||
btn->setAutoRepeat(true);
|
||||
} else if (p == ENTER_KEY) {
|
||||
btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #465BEA;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #444444;
|
||||
}
|
||||
)");
|
||||
}
|
||||
btn->setFixedHeight(135 + key_spacing_vertical);
|
||||
btn_group->addButton(btn);
|
||||
hlayout->addWidget(btn, KEY_STRETCH.value(p, 1));
|
||||
}
|
||||
|
||||
if (main_layout->count() == 1) {
|
||||
hlayout->addSpacing(90);
|
||||
}
|
||||
|
||||
main_layout->addLayout(hlayout);
|
||||
}
|
||||
|
||||
setStyleSheet(QString(R"(
|
||||
QPushButton {
|
||||
font-size: 75px;
|
||||
margin-left: %1px;
|
||||
margin-right: %1px;
|
||||
margin-top: %2px;
|
||||
margin-bottom: %2px;
|
||||
padding: 0px;
|
||||
border-radius: 10px;
|
||||
color: #dddddd;
|
||||
background-color: #444444;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #333333;
|
||||
}
|
||||
)").arg(key_spacing_vertical / 2).arg(key_spacing_horizontal / 2));
|
||||
}
|
||||
|
||||
Keyboard::Keyboard(QWidget *parent) : QFrame(parent) {
|
||||
main_layout = new QStackedLayout(this);
|
||||
main_layout->setMargin(0);
|
||||
|
||||
// lowercase
|
||||
std::vector<QVector<QString>> lowercase = {
|
||||
{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p"},
|
||||
{"a", "s", "d", "f", "g", "h", "j", "k", "l"},
|
||||
{"↑", "z", "x", "c", "v", "b", "n", "m", BACKSPACE_KEY},
|
||||
{"123", " ", ".", ENTER_KEY},
|
||||
};
|
||||
main_layout->addWidget(new KeyboardLayout(this, lowercase));
|
||||
|
||||
// uppercase
|
||||
std::vector<QVector<QString>> uppercase = {
|
||||
{"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"},
|
||||
{"A", "S", "D", "F", "G", "H", "J", "K", "L"},
|
||||
{"↓", "Z", "X", "C", "V", "B", "N", "M", BACKSPACE_KEY},
|
||||
{"123", " ", ".", ENTER_KEY},
|
||||
};
|
||||
main_layout->addWidget(new KeyboardLayout(this, uppercase));
|
||||
|
||||
// numbers + specials
|
||||
std::vector<QVector<QString>> numbers = {
|
||||
{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"},
|
||||
{"-", "/", ":", ";", "(", ")", "$", "&&", "@", "\""},
|
||||
{"#+=", ".", ",", "?", "!", "`", BACKSPACE_KEY},
|
||||
{"ABC", " ", ".", ENTER_KEY},
|
||||
};
|
||||
main_layout->addWidget(new KeyboardLayout(this, numbers));
|
||||
|
||||
// extra specials
|
||||
std::vector<QVector<QString>> specials = {
|
||||
{"[", "]", "{", "}", "#", "%", "^", "*", "+", "="},
|
||||
{"_", "\\", "|", "~", "<", ">", "€", "£", "¥", "•"},
|
||||
{"123", ".", ",", "?", "!", "'", BACKSPACE_KEY},
|
||||
{"ABC", " ", ".", ENTER_KEY},
|
||||
};
|
||||
main_layout->addWidget(new KeyboardLayout(this, specials));
|
||||
|
||||
main_layout->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
void Keyboard::handleButton(QAbstractButton* btn) {
|
||||
const QString &key = btn->text();
|
||||
if (CONTROL_BUTTONS.contains(key)) {
|
||||
if (key == "↓" || key == "ABC") {
|
||||
main_layout->setCurrentIndex(0);
|
||||
} else if (key == "↑") {
|
||||
main_layout->setCurrentIndex(1);
|
||||
} else if (key == "123") {
|
||||
main_layout->setCurrentIndex(2);
|
||||
} else if (key == "#+=") {
|
||||
main_layout->setCurrentIndex(3);
|
||||
} else if (key == ENTER_KEY) {
|
||||
main_layout->setCurrentIndex(0);
|
||||
emit emitEnter();
|
||||
} else if (key == BACKSPACE_KEY) {
|
||||
emit emitBackspace();
|
||||
}
|
||||
} else {
|
||||
if ("A" <= key && key <= "Z") {
|
||||
main_layout->setCurrentIndex(0);
|
||||
}
|
||||
emit emitKey(key);
|
||||
}
|
||||
}
|
||||
40
selfdrive/ui/qt/widgets/keyboard.h
Normal file
40
selfdrive/ui/qt/widgets/keyboard.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
|
||||
class KeyButton : public QPushButton {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
KeyButton(const QString &text, QWidget *parent = 0);
|
||||
bool event(QEvent *event) override;
|
||||
};
|
||||
|
||||
class KeyboardLayout : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KeyboardLayout(QWidget* parent, const std::vector<QVector<QString>>& layout);
|
||||
};
|
||||
|
||||
class Keyboard : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Keyboard(QWidget *parent = 0);
|
||||
|
||||
private:
|
||||
QStackedLayout* main_layout;
|
||||
|
||||
private slots:
|
||||
void handleButton(QAbstractButton* m_button);
|
||||
|
||||
signals:
|
||||
void emitKey(const QString &s);
|
||||
void emitBackspace();
|
||||
void emitEnter();
|
||||
};
|
||||
127
selfdrive/ui/qt/widgets/offroad_alerts.cc
Normal file
127
selfdrive/ui/qt/widgets/offroad_alerts.cc
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "common/util.h"
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setMargin(50);
|
||||
main_layout->setSpacing(30);
|
||||
|
||||
QWidget *widget = new QWidget;
|
||||
scrollable_layout = new QVBoxLayout(widget);
|
||||
widget->setStyleSheet("background-color: transparent;");
|
||||
main_layout->addWidget(new ScrollView(widget));
|
||||
|
||||
// bottom footer, dismiss + reboot buttons
|
||||
QHBoxLayout *footer_layout = new QHBoxLayout();
|
||||
main_layout->addLayout(footer_layout);
|
||||
|
||||
QPushButton *dismiss_btn = new QPushButton(tr("Close"));
|
||||
dismiss_btn->setFixedSize(400, 125);
|
||||
footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft);
|
||||
QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss);
|
||||
|
||||
snooze_btn = new QPushButton(tr("Snooze Update"));
|
||||
snooze_btn->setVisible(false);
|
||||
snooze_btn->setFixedSize(550, 125);
|
||||
footer_layout->addWidget(snooze_btn, 0, Qt::AlignBottom | Qt::AlignRight);
|
||||
QObject::connect(snooze_btn, &QPushButton::clicked, [=]() {
|
||||
params.putBool("SnoozeUpdate", true);
|
||||
});
|
||||
QObject::connect(snooze_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss);
|
||||
snooze_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)");
|
||||
|
||||
if (hasRebootBtn) {
|
||||
QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update"));
|
||||
rebootBtn->setFixedSize(600, 125);
|
||||
footer_layout->addWidget(rebootBtn, 0, Qt::AlignBottom | Qt::AlignRight);
|
||||
QObject::connect(rebootBtn, &QPushButton::clicked, [=]() { Hardware::reboot(); });
|
||||
}
|
||||
|
||||
setStyleSheet(R"(
|
||||
* {
|
||||
font-size: 48px;
|
||||
color: white;
|
||||
}
|
||||
QFrame {
|
||||
border-radius: 30px;
|
||||
background-color: #393939;
|
||||
}
|
||||
QPushButton {
|
||||
color: black;
|
||||
font-weight: 500;
|
||||
border-radius: 30px;
|
||||
background-color: white;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
int OffroadAlert::refresh() {
|
||||
// build widgets for each offroad alert on first refresh
|
||||
if (alerts.empty()) {
|
||||
QString json = util::read_file("../controls/lib/alerts_offroad.json").c_str();
|
||||
QJsonObject obj = QJsonDocument::fromJson(json.toUtf8()).object();
|
||||
|
||||
// descending sort labels by severity
|
||||
std::vector<std::pair<std::string, int>> sorted;
|
||||
for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) {
|
||||
sorted.push_back({it.key().toStdString(), it.value()["severity"].toInt()});
|
||||
}
|
||||
std::sort(sorted.begin(), sorted.end(), [=](auto &l, auto &r) { return l.second > r.second; });
|
||||
|
||||
for (auto &[key, severity] : sorted) {
|
||||
QLabel *l = new QLabel(this);
|
||||
alerts[key] = l;
|
||||
l->setMargin(60);
|
||||
l->setWordWrap(true);
|
||||
l->setStyleSheet(QString("background-color: %1").arg(severity ? "#E22C2C" : "#292929"));
|
||||
scrollable_layout->addWidget(l);
|
||||
}
|
||||
scrollable_layout->addStretch(1);
|
||||
}
|
||||
|
||||
int alertCount = 0;
|
||||
for (const auto &[key, label] : alerts) {
|
||||
QString text;
|
||||
std::string bytes = params.get(key);
|
||||
if (bytes.size()) {
|
||||
auto doc_par = QJsonDocument::fromJson(bytes.c_str());
|
||||
text = tr(doc_par["text"].toString().toUtf8().data());
|
||||
auto extra = doc_par["extra"].toString();
|
||||
if (!extra.isEmpty()) {
|
||||
text = text.arg(extra);
|
||||
}
|
||||
}
|
||||
label->setText(text);
|
||||
label->setVisible(!text.isEmpty());
|
||||
alertCount += !text.isEmpty();
|
||||
}
|
||||
snooze_btn->setVisible(!alerts["Offroad_ConnectivityNeeded"]->text().isEmpty());
|
||||
return alertCount;
|
||||
}
|
||||
|
||||
UpdateAlert::UpdateAlert(QWidget *parent) : AbstractAlert(true, parent) {
|
||||
releaseNotes = new QLabel(this);
|
||||
releaseNotes->setWordWrap(true);
|
||||
releaseNotes->setAlignment(Qt::AlignTop);
|
||||
scrollable_layout->addWidget(releaseNotes);
|
||||
}
|
||||
|
||||
bool UpdateAlert::refresh() {
|
||||
bool updateAvailable = params.getBool("UpdateAvailable");
|
||||
if (updateAvailable) {
|
||||
releaseNotes->setText(params.get("UpdaterNewReleaseNotes").c_str());
|
||||
}
|
||||
return updateAvailable;
|
||||
}
|
||||
46
selfdrive/ui/qt/widgets/offroad_alerts.h
Normal file
46
selfdrive/ui/qt/widgets/offroad_alerts.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "common/params.h"
|
||||
|
||||
class AbstractAlert : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
protected:
|
||||
AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr);
|
||||
|
||||
QPushButton *snooze_btn;
|
||||
QVBoxLayout *scrollable_layout;
|
||||
Params params;
|
||||
|
||||
signals:
|
||||
void dismiss();
|
||||
};
|
||||
|
||||
class UpdateAlert : public AbstractAlert {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
UpdateAlert(QWidget *parent = 0);
|
||||
bool refresh();
|
||||
|
||||
private:
|
||||
QLabel *releaseNotes = nullptr;
|
||||
};
|
||||
|
||||
class OffroadAlert : public AbstractAlert {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {}
|
||||
int refresh();
|
||||
|
||||
private:
|
||||
std::map<std::string, QLabel*> alerts;
|
||||
};
|
||||
283
selfdrive/ui/qt/widgets/prime.cc
Normal file
283
selfdrive/ui/qt/widgets/prime.cc
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "selfdrive/ui/qt/widgets/prime.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedWidget>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <QrCode.hpp>
|
||||
|
||||
#include "selfdrive/ui/qt/request_repeater.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/qt_window.h"
|
||||
#include "selfdrive/ui/qt/widgets/wifi.h"
|
||||
|
||||
using qrcodegen::QrCode;
|
||||
|
||||
PairingQRWidget::PairingQRWidget(QWidget* parent) : QWidget(parent) {
|
||||
timer = new QTimer(this);
|
||||
connect(timer, &QTimer::timeout, this, &PairingQRWidget::refresh);
|
||||
}
|
||||
|
||||
void PairingQRWidget::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 60 * 1000);
|
||||
device()->setOffroadBrightness(100);
|
||||
}
|
||||
|
||||
void PairingQRWidget::hideEvent(QHideEvent *event) {
|
||||
timer->stop();
|
||||
device()->setOffroadBrightness(BACKLIGHT_OFFROAD);
|
||||
}
|
||||
|
||||
void PairingQRWidget::refresh() {
|
||||
QString pairToken = CommaApi::create_jwt({{"pair", true}});
|
||||
QString qrString = "https://connect.comma.ai/?pair=" + pairToken;
|
||||
this->updateQrCode(qrString);
|
||||
update();
|
||||
}
|
||||
|
||||
void PairingQRWidget::updateQrCode(const QString &text) {
|
||||
QrCode qr = QrCode::encodeText(text.toUtf8().data(), QrCode::Ecc::LOW);
|
||||
qint32 sz = qr.getSize();
|
||||
QImage im(sz, sz, QImage::Format_RGB32);
|
||||
|
||||
QRgb black = qRgb(0, 0, 0);
|
||||
QRgb white = qRgb(255, 255, 255);
|
||||
for (int y = 0; y < sz; y++) {
|
||||
for (int x = 0; x < sz; x++) {
|
||||
im.setPixel(x, y, qr.getModule(x, y) ? black : white);
|
||||
}
|
||||
}
|
||||
|
||||
// Integer division to prevent anti-aliasing
|
||||
int final_sz = ((width() / sz) - 1) * sz;
|
||||
img = QPixmap::fromImage(im.scaled(final_sz, final_sz, Qt::KeepAspectRatio), Qt::MonoOnly);
|
||||
}
|
||||
|
||||
void PairingQRWidget::paintEvent(QPaintEvent *e) {
|
||||
QPainter p(this);
|
||||
p.fillRect(rect(), Qt::white);
|
||||
|
||||
QSize s = (size() - img.size()) / 2;
|
||||
p.drawPixmap(s.width(), s.height(), img);
|
||||
}
|
||||
|
||||
|
||||
PairingPopup::PairingPopup(QWidget *parent) : DialogBase(parent) {
|
||||
QHBoxLayout *hlayout = new QHBoxLayout(this);
|
||||
hlayout->setContentsMargins(0, 0, 0, 0);
|
||||
hlayout->setSpacing(0);
|
||||
|
||||
setStyleSheet("PairingPopup { background-color: #E0E0E0; }");
|
||||
|
||||
// text
|
||||
QVBoxLayout *vlayout = new QVBoxLayout();
|
||||
vlayout->setContentsMargins(85, 70, 50, 70);
|
||||
vlayout->setSpacing(50);
|
||||
hlayout->addLayout(vlayout, 1);
|
||||
{
|
||||
QPushButton *close = new QPushButton(QIcon(":/icons/close.svg"), "", this);
|
||||
close->setIconSize(QSize(80, 80));
|
||||
close->setStyleSheet("border: none;");
|
||||
vlayout->addWidget(close, 0, Qt::AlignLeft);
|
||||
QObject::connect(close, &QPushButton::clicked, this, &QDialog::reject);
|
||||
|
||||
vlayout->addSpacing(30);
|
||||
|
||||
QLabel *title = new QLabel(tr("Pair your device to your comma account"), this);
|
||||
title->setStyleSheet("font-size: 75px; color: black;");
|
||||
title->setWordWrap(true);
|
||||
vlayout->addWidget(title);
|
||||
|
||||
QLabel *instructions = new QLabel(QString(R"(
|
||||
<ol type='1' style='margin-left: 15px;'>
|
||||
<li style='margin-bottom: 50px;'>%1</li>
|
||||
<li style='margin-bottom: 50px;'>%2</li>
|
||||
<li style='margin-bottom: 50px;'>%3</li>
|
||||
</ol>
|
||||
)").arg(tr("Go to https://connect.comma.ai on your phone"))
|
||||
.arg(tr("Click \"add new device\" and scan the QR code on the right"))
|
||||
.arg(tr("Bookmark connect.comma.ai to your home screen to use it like an app")), this);
|
||||
|
||||
instructions->setStyleSheet("font-size: 47px; font-weight: bold; color: black;");
|
||||
instructions->setWordWrap(true);
|
||||
vlayout->addWidget(instructions);
|
||||
|
||||
vlayout->addStretch();
|
||||
}
|
||||
|
||||
// QR code
|
||||
PairingQRWidget *qr = new PairingQRWidget(this);
|
||||
hlayout->addWidget(qr, 1);
|
||||
}
|
||||
|
||||
|
||||
PrimeUserWidget::PrimeUserWidget(QWidget *parent) : QFrame(parent) {
|
||||
setObjectName("primeWidget");
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(56, 40, 56, 40);
|
||||
mainLayout->setSpacing(20);
|
||||
|
||||
QLabel *subscribed = new QLabel(tr("✓ SUBSCRIBED"));
|
||||
subscribed->setStyleSheet("font-size: 41px; font-weight: bold; color: #86FF4E;");
|
||||
mainLayout->addWidget(subscribed);
|
||||
|
||||
QLabel *commaPrime = new QLabel(tr("comma prime"));
|
||||
commaPrime->setStyleSheet("font-size: 75px; font-weight: bold;");
|
||||
mainLayout->addWidget(commaPrime);
|
||||
}
|
||||
|
||||
|
||||
PrimeAdWidget::PrimeAdWidget(QWidget* parent) : QFrame(parent) {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(80, 90, 80, 60);
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
QLabel *upgrade = new QLabel(tr("Upgrade Now"));
|
||||
upgrade->setStyleSheet("font-size: 75px; font-weight: bold;");
|
||||
main_layout->addWidget(upgrade, 0, Qt::AlignTop);
|
||||
main_layout->addSpacing(50);
|
||||
|
||||
QLabel *description = new QLabel(tr("Become a comma prime member at connect.comma.ai"));
|
||||
description->setStyleSheet("font-size: 56px; font-weight: light; color: white;");
|
||||
description->setWordWrap(true);
|
||||
main_layout->addWidget(description, 0, Qt::AlignTop);
|
||||
|
||||
main_layout->addStretch();
|
||||
|
||||
QLabel *features = new QLabel(tr("PRIME FEATURES:"));
|
||||
features->setStyleSheet("font-size: 41px; font-weight: bold; color: #E5E5E5;");
|
||||
main_layout->addWidget(features, 0, Qt::AlignBottom);
|
||||
main_layout->addSpacing(30);
|
||||
|
||||
QVector<QString> bullets = {tr("Remote access"), tr("24/7 LTE connectivity"), tr("1 year of drive storage"), tr("Turn-by-turn navigation")};
|
||||
for (auto &b : bullets) {
|
||||
const QString check = "<b><font color='#465BEA'>✓</font></b> ";
|
||||
QLabel *l = new QLabel(check + b);
|
||||
l->setAlignment(Qt::AlignLeft);
|
||||
l->setStyleSheet("font-size: 50px; margin-bottom: 15px;");
|
||||
main_layout->addWidget(l, 0, Qt::AlignBottom);
|
||||
}
|
||||
|
||||
setStyleSheet(R"(
|
||||
PrimeAdWidget {
|
||||
border-radius: 10px;
|
||||
background-color: #333333;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
|
||||
SetupWidget::SetupWidget(QWidget* parent) : QFrame(parent) {
|
||||
mainLayout = new QStackedWidget;
|
||||
|
||||
// Unpaired, registration prompt layout
|
||||
|
||||
QFrame* finishRegistration = new QFrame;
|
||||
finishRegistration->setObjectName("primeWidget");
|
||||
QVBoxLayout* finishRegistationLayout = new QVBoxLayout(finishRegistration);
|
||||
finishRegistationLayout->setSpacing(38);
|
||||
finishRegistationLayout->setContentsMargins(64, 48, 64, 48);
|
||||
|
||||
QLabel* registrationTitle = new QLabel(tr("Finish Setup"));
|
||||
registrationTitle->setStyleSheet("font-size: 75px; font-weight: bold;");
|
||||
finishRegistationLayout->addWidget(registrationTitle);
|
||||
|
||||
QLabel* registrationDescription = new QLabel(tr("Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer."));
|
||||
registrationDescription->setWordWrap(true);
|
||||
registrationDescription->setStyleSheet("font-size: 50px; font-weight: light;");
|
||||
finishRegistationLayout->addWidget(registrationDescription);
|
||||
|
||||
finishRegistationLayout->addStretch();
|
||||
|
||||
QPushButton* pair = new QPushButton(tr("Pair device"));
|
||||
pair->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 55px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
background-color: #465BEA;
|
||||
padding: 64px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #3049F4;
|
||||
}
|
||||
)");
|
||||
finishRegistationLayout->addWidget(pair);
|
||||
|
||||
popup = new PairingPopup(this);
|
||||
QObject::connect(pair, &QPushButton::clicked, popup, &PairingPopup::exec);
|
||||
|
||||
mainLayout->addWidget(finishRegistration);
|
||||
|
||||
// build stacked layout
|
||||
QVBoxLayout *outer_layout = new QVBoxLayout(this);
|
||||
outer_layout->setContentsMargins(0, 0, 0, 0);
|
||||
outer_layout->addWidget(mainLayout);
|
||||
|
||||
QWidget *content = new QWidget;
|
||||
QVBoxLayout *content_layout = new QVBoxLayout(content);
|
||||
content_layout->setContentsMargins(0, 0, 0, 0);
|
||||
content_layout->setSpacing(30);
|
||||
|
||||
primeUser = new PrimeUserWidget;
|
||||
content_layout->addWidget(primeUser);
|
||||
|
||||
WiFiPromptWidget *wifi_prompt = new WiFiPromptWidget;
|
||||
QObject::connect(wifi_prompt, &WiFiPromptWidget::openSettings, this, &SetupWidget::openSettings);
|
||||
content_layout->addWidget(wifi_prompt);
|
||||
content_layout->addStretch();
|
||||
|
||||
mainLayout->addWidget(content);
|
||||
|
||||
primeUser->setVisible(uiState()->primeType());
|
||||
mainLayout->setCurrentIndex(1);
|
||||
|
||||
setStyleSheet(R"(
|
||||
#primeWidget {
|
||||
border-radius: 10px;
|
||||
background-color: #333333;
|
||||
}
|
||||
)");
|
||||
|
||||
// Retain size while hidden
|
||||
QSizePolicy sp_retain = sizePolicy();
|
||||
sp_retain.setRetainSizeWhenHidden(true);
|
||||
setSizePolicy(sp_retain);
|
||||
|
||||
// set up API requests
|
||||
if (auto dongleId = getDongleId()) {
|
||||
QString url = CommaApi::BASE_URL + "/v1.1/devices/" + *dongleId + "/";
|
||||
RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_Device", 5);
|
||||
|
||||
QObject::connect(repeater, &RequestRepeater::requestDone, this, &SetupWidget::replyFinished);
|
||||
}
|
||||
}
|
||||
|
||||
void SetupWidget::replyFinished(const QString &response, bool success) {
|
||||
if (!success) return;
|
||||
|
||||
QJsonDocument doc = QJsonDocument::fromJson(response.toUtf8());
|
||||
if (doc.isNull()) {
|
||||
qDebug() << "JSON Parse failed on getting pairing and prime status";
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject json = doc.object();
|
||||
PrimeType prime_type = static_cast<PrimeType>(json["prime_type"].toInt());
|
||||
uiState()->setPrimeType(prime_type);
|
||||
|
||||
if (!json["is_paired"].toBool()) {
|
||||
mainLayout->setCurrentIndex(0);
|
||||
} else {
|
||||
popup->reject();
|
||||
|
||||
primeUser->setVisible(prime_type);
|
||||
mainLayout->setCurrentIndex(1);
|
||||
}
|
||||
}
|
||||
73
selfdrive/ui/qt/widgets/prime.h
Normal file
73
selfdrive/ui/qt/widgets/prime.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <QLabel>
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
|
||||
// pairing QR code
|
||||
class PairingQRWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PairingQRWidget(QWidget* parent = 0);
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
private:
|
||||
QPixmap img;
|
||||
QTimer *timer;
|
||||
void updateQrCode(const QString &text);
|
||||
void showEvent(QShowEvent *event) override;
|
||||
void hideEvent(QHideEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
};
|
||||
|
||||
|
||||
// pairing popup widget
|
||||
class PairingPopup : public DialogBase {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PairingPopup(QWidget* parent);
|
||||
};
|
||||
|
||||
|
||||
// widget for paired users with prime
|
||||
class PrimeUserWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PrimeUserWidget(QWidget* parent = 0);
|
||||
};
|
||||
|
||||
|
||||
// widget for paired users without prime
|
||||
class PrimeAdWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PrimeAdWidget(QWidget* parent = 0);
|
||||
};
|
||||
|
||||
|
||||
// container widget
|
||||
class SetupWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SetupWidget(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
|
||||
private:
|
||||
PairingPopup *popup;
|
||||
QStackedWidget *mainLayout;
|
||||
PrimeUserWidget *primeUser;
|
||||
|
||||
private slots:
|
||||
void replyFinished(const QString &response, bool success);
|
||||
};
|
||||
49
selfdrive/ui/qt/widgets/scrollview.cc
Normal file
49
selfdrive/ui/qt/widgets/scrollview.cc
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
#include <QScrollBar>
|
||||
#include <QScroller>
|
||||
|
||||
// TODO: disable horizontal scrolling and resize
|
||||
|
||||
ScrollView::ScrollView(QWidget *w, QWidget *parent) : QScrollArea(parent) {
|
||||
setWidget(w);
|
||||
setWidgetResizable(true);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
setStyleSheet("background-color: transparent; border:none");
|
||||
|
||||
QString style = R"(
|
||||
QScrollBar:vertical {
|
||||
border: none;
|
||||
background: transparent;
|
||||
width: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
min-height: 0px;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
}
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
}
|
||||
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
)";
|
||||
verticalScrollBar()->setStyleSheet(style);
|
||||
horizontalScrollBar()->setStyleSheet(style);
|
||||
|
||||
QScroller *scroller = QScroller::scroller(this->viewport());
|
||||
QScrollerProperties sp = scroller->scrollerProperties();
|
||||
|
||||
sp.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QVariant::fromValue<QScrollerProperties::OvershootPolicy>(QScrollerProperties::OvershootAlwaysOff));
|
||||
sp.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QVariant::fromValue<QScrollerProperties::OvershootPolicy>(QScrollerProperties::OvershootAlwaysOff));
|
||||
sp.setScrollMetric(QScrollerProperties::MousePressEventDelay, 0.01);
|
||||
scroller->grabGesture(this->viewport(), QScroller::LeftMouseButtonGesture);
|
||||
scroller->setScrollerProperties(sp);
|
||||
}
|
||||
|
||||
void ScrollView::hideEvent(QHideEvent *e) {
|
||||
verticalScrollBar()->setValue(0);
|
||||
}
|
||||
12
selfdrive/ui/qt/widgets/scrollview.h
Normal file
12
selfdrive/ui/qt/widgets/scrollview.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <QScrollArea>
|
||||
|
||||
class ScrollView : public QScrollArea {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ScrollView(QWidget *w = nullptr, QWidget *parent = nullptr);
|
||||
protected:
|
||||
void hideEvent(QHideEvent *e) override;
|
||||
};
|
||||
64
selfdrive/ui/qt/widgets/ssh_keys.cc
Normal file
64
selfdrive/ui/qt/widgets/ssh_keys.cc
Normal file
@@ -0,0 +1,64 @@
|
||||
#include "selfdrive/ui/qt/widgets/ssh_keys.h"
|
||||
|
||||
#include "common/params.h"
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
#include "selfdrive/ui/qt/widgets/input.h"
|
||||
|
||||
SshControl::SshControl() :
|
||||
ButtonControl(tr("SSH Keys"), "", tr("Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username "
|
||||
"other than your own. A comma employee will NEVER ask you to add their GitHub username.")) {
|
||||
|
||||
QObject::connect(this, &ButtonControl::clicked, [=]() {
|
||||
if (text() == tr("ADD")) {
|
||||
QString username = InputDialog::getText(tr("Enter your GitHub username"), this);
|
||||
if (username.length() > 0) {
|
||||
setText(tr("LOADING"));
|
||||
setEnabled(false);
|
||||
getUserKeys(username);
|
||||
}
|
||||
} else {
|
||||
params.remove("GithubUsername");
|
||||
params.remove("GithubSshKeys");
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SshControl::refresh() {
|
||||
QString param = QString::fromStdString(params.get("GithubSshKeys"));
|
||||
if (param.length()) {
|
||||
setValue(QString::fromStdString(params.get("GithubUsername")));
|
||||
setText(tr("REMOVE"));
|
||||
} else {
|
||||
setValue("");
|
||||
setText(tr("ADD"));
|
||||
}
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
void SshControl::getUserKeys(const QString &username) {
|
||||
HttpRequest *request = new HttpRequest(this, false);
|
||||
QObject::connect(request, &HttpRequest::requestDone, [=](const QString &resp, bool success) {
|
||||
if (success) {
|
||||
if (!resp.isEmpty()) {
|
||||
params.put("GithubUsername", username.toStdString());
|
||||
params.put("GithubSshKeys", resp.toStdString());
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Username '%1' has no keys on GitHub").arg(username), this);
|
||||
}
|
||||
} else {
|
||||
if (request->timeout()) {
|
||||
ConfirmationDialog::alert(tr("Request timed out"), this);
|
||||
} else {
|
||||
ConfirmationDialog::alert(tr("Username '%1' doesn't exist on GitHub").arg(username), this);
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
request->deleteLater();
|
||||
});
|
||||
|
||||
request->sendRequest("https://github.com/" + username + ".keys");
|
||||
}
|
||||
32
selfdrive/ui/qt/widgets/ssh_keys.h
Normal file
32
selfdrive/ui/qt/widgets/ssh_keys.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
#include "selfdrive/ui/qt/widgets/controls.h"
|
||||
|
||||
// SSH enable toggle
|
||||
class SshToggle : public ToggleControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SshToggle() : ToggleControl(tr("Enable SSH"), "", "", Hardware::get_ssh_enabled()) {
|
||||
QObject::connect(this, &SshToggle::toggleFlipped, [=](bool state) {
|
||||
Hardware::set_ssh_enabled(state);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// SSH key management widget
|
||||
class SshControl : public ButtonControl {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SshControl();
|
||||
|
||||
private:
|
||||
Params params;
|
||||
|
||||
void refresh();
|
||||
void getUserKeys(const QString &username);
|
||||
};
|
||||
83
selfdrive/ui/qt/widgets/toggle.cc
Normal file
83
selfdrive/ui/qt/widgets/toggle.cc
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "selfdrive/ui/qt/widgets/toggle.h"
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
Toggle::Toggle(QWidget *parent) : QAbstractButton(parent),
|
||||
_height(80),
|
||||
_height_rect(60),
|
||||
on(false),
|
||||
_anim(new QPropertyAnimation(this, "offset_circle", this))
|
||||
{
|
||||
_radius = _height / 2;
|
||||
_x_circle = _radius;
|
||||
_y_circle = _radius;
|
||||
_y_rect = (_height - _height_rect)/2;
|
||||
circleColor = QColor(0xffffff); // placeholder
|
||||
green = QColor(0xffffff); // placeholder
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
void Toggle::paintEvent(QPaintEvent *e) {
|
||||
this->setFixedHeight(_height);
|
||||
QPainter p(this);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
// Draw toggle background left
|
||||
p.setBrush(green);
|
||||
p.drawRoundedRect(QRect(0, _y_rect, _x_circle + _radius, _height_rect), _height_rect/2, _height_rect/2);
|
||||
|
||||
// Draw toggle background right
|
||||
p.setBrush(QColor(0x393939));
|
||||
p.drawRoundedRect(QRect(_x_circle - _radius, _y_rect, width() - (_x_circle - _radius), _height_rect), _height_rect/2, _height_rect/2);
|
||||
|
||||
// Draw toggle circle
|
||||
p.setBrush(circleColor);
|
||||
p.drawEllipse(QRectF(_x_circle - _radius, _y_circle - _radius, 2 * _radius, 2 * _radius));
|
||||
}
|
||||
|
||||
void Toggle::mouseReleaseEvent(QMouseEvent *e) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const int left = _radius;
|
||||
const int right = width() - _radius;
|
||||
if ((_x_circle != left && _x_circle != right) || !this->rect().contains(e->localPos().toPoint())) {
|
||||
// If mouse release isn't in rect or animation is running, don't parse touch events
|
||||
return;
|
||||
}
|
||||
if (e->button() & Qt::LeftButton) {
|
||||
togglePosition();
|
||||
emit stateChanged(on);
|
||||
}
|
||||
}
|
||||
|
||||
void Toggle::togglePosition() {
|
||||
on = !on;
|
||||
const int left = _radius;
|
||||
const int right = width() - _radius;
|
||||
_anim->setStartValue(on ? left + immediateOffset : right - immediateOffset);
|
||||
_anim->setEndValue(on ? right : left);
|
||||
_anim->setDuration(animation_duration);
|
||||
_anim->start();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void Toggle::enterEvent(QEvent *e) {
|
||||
QAbstractButton::enterEvent(e);
|
||||
}
|
||||
|
||||
bool Toggle::getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void Toggle::setEnabled(bool value) {
|
||||
enabled = value;
|
||||
if (value) {
|
||||
circleColor.setRgb(0xfafafa);
|
||||
green.setRgb(0x33ab4c);
|
||||
} else {
|
||||
circleColor.setRgb(0x888888);
|
||||
green.setRgb(0x227722);
|
||||
}
|
||||
}
|
||||
44
selfdrive/ui/qt/widgets/toggle.h
Normal file
44
selfdrive/ui/qt/widgets/toggle.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QMouseEvent>
|
||||
#include <QPropertyAnimation>
|
||||
|
||||
class Toggle : public QAbstractButton {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int offset_circle READ offset_circle WRITE set_offset_circle CONSTANT)
|
||||
|
||||
public:
|
||||
Toggle(QWidget* parent = nullptr);
|
||||
void togglePosition();
|
||||
bool on;
|
||||
int animation_duration = 150;
|
||||
int immediateOffset = 0;
|
||||
int offset_circle() const {
|
||||
return _x_circle;
|
||||
}
|
||||
|
||||
void set_offset_circle(int o) {
|
||||
_x_circle = o;
|
||||
update();
|
||||
}
|
||||
bool getEnabled();
|
||||
void setEnabled(bool value);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
void mouseReleaseEvent(QMouseEvent*) override;
|
||||
void enterEvent(QEvent*) override;
|
||||
|
||||
private:
|
||||
QColor circleColor;
|
||||
QColor green;
|
||||
bool enabled = true;
|
||||
int _x_circle, _y_circle;
|
||||
int _height, _radius;
|
||||
int _height_rect, _y_rect;
|
||||
QPropertyAnimation *_anim = nullptr;
|
||||
|
||||
signals:
|
||||
void stateChanged(bool new_state);
|
||||
};
|
||||
103
selfdrive/ui/qt/widgets/wifi.cc
Normal file
103
selfdrive/ui/qt/widgets/wifi.cc
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "selfdrive/ui/qt/widgets/wifi.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
|
||||
WiFiPromptWidget::WiFiPromptWidget(QWidget *parent) : QFrame(parent) {
|
||||
stack = new QStackedLayout(this);
|
||||
|
||||
// Setup Wi-Fi
|
||||
QFrame *setup = new QFrame;
|
||||
QVBoxLayout *setup_layout = new QVBoxLayout(setup);
|
||||
setup_layout->setContentsMargins(56, 40, 56, 40);
|
||||
setup_layout->setSpacing(20);
|
||||
{
|
||||
QHBoxLayout *title_layout = new QHBoxLayout;
|
||||
title_layout->setSpacing(32);
|
||||
{
|
||||
QLabel *icon = new QLabel;
|
||||
QPixmap pixmap("../assets/offroad/icon_wifi_strength_full.svg");
|
||||
icon->setPixmap(pixmap.scaledToWidth(80, Qt::SmoothTransformation));
|
||||
title_layout->addWidget(icon);
|
||||
|
||||
QLabel *title = new QLabel(tr("Setup Wi-Fi"));
|
||||
title->setStyleSheet("font-size: 64px; font-weight: 600;");
|
||||
title_layout->addWidget(title);
|
||||
title_layout->addStretch();
|
||||
}
|
||||
setup_layout->addLayout(title_layout);
|
||||
|
||||
QLabel *desc = new QLabel(tr("Connect to Wi-Fi to upload driving data and help improve openpilot"));
|
||||
desc->setStyleSheet("font-size: 40px; font-weight: 400;");
|
||||
desc->setWordWrap(true);
|
||||
setup_layout->addWidget(desc);
|
||||
|
||||
QPushButton *settings_btn = new QPushButton(tr("Open Settings"));
|
||||
connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(1); });
|
||||
settings_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
font-size: 48px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
background-color: #465BEA;
|
||||
padding: 32px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #3049F4;
|
||||
}
|
||||
)");
|
||||
setup_layout->addWidget(settings_btn);
|
||||
}
|
||||
stack->addWidget(setup);
|
||||
|
||||
// Uploading data
|
||||
QWidget *uploading = new QWidget;
|
||||
QVBoxLayout *uploading_layout = new QVBoxLayout(uploading);
|
||||
uploading_layout->setContentsMargins(64, 56, 64, 56);
|
||||
uploading_layout->setSpacing(36);
|
||||
{
|
||||
QHBoxLayout *title_layout = new QHBoxLayout;
|
||||
{
|
||||
QLabel *title = new QLabel(tr("Ready to upload"));
|
||||
title->setStyleSheet("font-size: 64px; font-weight: 600;");
|
||||
title->setWordWrap(true);
|
||||
title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
|
||||
title_layout->addWidget(title);
|
||||
title_layout->addStretch();
|
||||
|
||||
QLabel *icon = new QLabel;
|
||||
QPixmap pixmap("../assets/offroad/icon_wifi_uploading.svg");
|
||||
icon->setPixmap(pixmap.scaledToWidth(120, Qt::SmoothTransformation));
|
||||
title_layout->addWidget(icon);
|
||||
}
|
||||
uploading_layout->addLayout(title_layout);
|
||||
|
||||
QLabel *desc = new QLabel(tr("Training data will be pulled periodically while your device is on Wi-Fi"));
|
||||
desc->setStyleSheet("font-size: 48px; font-weight: 400;");
|
||||
desc->setWordWrap(true);
|
||||
uploading_layout->addWidget(desc);
|
||||
}
|
||||
stack->addWidget(uploading);
|
||||
|
||||
setStyleSheet(R"(
|
||||
WiFiPromptWidget {
|
||||
background-color: #333333;
|
||||
border-radius: 10px;
|
||||
}
|
||||
)");
|
||||
|
||||
QObject::connect(uiState(), &UIState::uiUpdate, this, &WiFiPromptWidget::updateState);
|
||||
}
|
||||
|
||||
void WiFiPromptWidget::updateState(const UIState &s) {
|
||||
if (!isVisible()) return;
|
||||
|
||||
auto &sm = *(s.sm);
|
||||
|
||||
auto network_type = sm["deviceState"].getDeviceState().getNetworkType();
|
||||
auto uploading = network_type == cereal::DeviceState::NetworkType::WIFI ||
|
||||
network_type == cereal::DeviceState::NetworkType::ETHERNET;
|
||||
stack->setCurrentIndex(uploading ? 1 : 0);
|
||||
}
|
||||
23
selfdrive/ui/qt/widgets/wifi.h
Normal file
23
selfdrive/ui/qt/widgets/wifi.h
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QStackedLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class WiFiPromptWidget : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WiFiPromptWidget(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
|
||||
public slots:
|
||||
void updateState(const UIState &s);
|
||||
|
||||
protected:
|
||||
QStackedLayout *stack;
|
||||
};
|
||||
Reference in New Issue
Block a user