UI: improved nav destination entry (#28476)

old-commit-hash: bc5a899024
beeps
Cameron Clough 2 years ago committed by GitHub
parent 1b3ba1c7ee
commit b6e6f008f1
  1. 3
      selfdrive/assets/navigation/icon_directions.svg
  2. 3
      selfdrive/assets/navigation/icon_directions_outlined.svg
  3. 3
      selfdrive/assets/navigation/icon_favorite.svg
  4. 3
      selfdrive/assets/navigation/icon_home.svg
  5. 3
      selfdrive/assets/navigation/icon_recent.svg
  6. 3
      selfdrive/assets/navigation/icon_settings.svg
  7. 3
      selfdrive/assets/navigation/icon_work.svg
  8. 3
      selfdrive/assets/navigation/screenshot.png
  9. 2
      selfdrive/ui/SConscript
  10. 14
      selfdrive/ui/qt/home.cc
  11. 40
      selfdrive/ui/qt/maps/map.cc
  12. 7
      selfdrive/ui/qt/maps/map.h
  13. 34
      selfdrive/ui/qt/maps/map_panel.cc
  14. 17
      selfdrive/ui/qt/maps/map_panel.h
  15. 488
      selfdrive/ui/qt/maps/map_settings.cc
  16. 100
      selfdrive/ui/qt/maps/map_settings.h
  17. 10
      selfdrive/ui/qt/offroad/settings.cc
  18. 16
      selfdrive/ui/qt/onroad.cc
  19. 18
      selfdrive/ui/qt/widgets/controls.cc
  20. 14
      selfdrive/ui/qt/widgets/controls.h
  21. 69
      selfdrive/ui/translations/main_de.ts
  22. 71
      selfdrive/ui/translations/main_ja.ts
  23. 71
      selfdrive/ui/translations/main_ko.ts
  24. 71
      selfdrive/ui/translations/main_pt-BR.ts
  25. 69
      selfdrive/ui/translations/main_zh-CHS.ts
  26. 71
      selfdrive/ui/translations/main_zh-CHT.ts

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:935cd01600d56350cda1941cf382c7c86cd959fa7a0a574bdba8b3011a350578
size 466

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3284def3e6e8b2683b7a13eaaa86b9555a8cce2ff6e7144ce9392c451c77c3d7
size 757

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff184bf0239c54060ffbaf465573f17cbc92bd8d3d38ce10e1bd02cdd2b21575
size 315

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed
size 489

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5fbce167d2005d08e8bc2113e0a9d5d3e2ed113db8e2a020df1ee14633cd3eed
size 1279

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1fa72d94b5e05884129dd502babd5de78666c349bb5d12e19872fdbc9fe2100e
size 910

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7ea949d3b66896ac3404d20aea9445a5cd00fefd18623f1c7160293a1de5f807
size 364

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bdddd78a5899f475f0fdddf9ecef9a3d13027d0d1162b493baa9b4299a8f7064
size 1461575

@ -29,7 +29,7 @@ widgets_src = ["ui.cc", "qt/widgets/input.cc", "qt/widgets/drive_stats.cc", "qt/
qt_env['CPPDEFINES'] = []
if maps:
base_libs += ['qmapboxgl']
widgets_src += ["qt/maps/map_helpers.cc", "qt/maps/map_settings.cc", "qt/maps/map.cc"]
widgets_src += ["qt/maps/map_helpers.cc", "qt/maps/map_settings.cc", "qt/maps/map.cc", "qt/maps/map_panel.cc"]
qt_env['CPPDEFINES'] += ["ENABLE_MAPS"]
widgets = qt_env.Library("qt_widgets", widgets_src, LIBS=base_libs)

@ -7,9 +7,14 @@
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/drive_stats.h"
#include "selfdrive/ui/qt/widgets/prime.h"
#ifdef ENABLE_MAPS
#include "selfdrive/ui/qt/maps/map_settings.h"
#else
#include "selfdrive/ui/qt/widgets/drive_stats.h"
#endif
// HomeWindow: the container for the offroad and onroad UIs
HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
@ -137,10 +142,15 @@ OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
home_layout->setContentsMargins(0, 0, 0, 0);
home_layout->setSpacing(30);
// left: DriveStats/PrimeAdWidget
// left: MapSettings/PrimeAdWidget
QStackedWidget *left_widget = new QStackedWidget(this);
#ifdef ENABLE_MAPS
left_widget->addWidget(new MapSettings);
#else
left_widget->addWidget(new DriveStats);
#endif
left_widget->addWidget(new PrimeAdWidget);
left_widget->setStyleSheet("border-radius: 10px;");
left_widget->setCurrentIndex(uiState()->primeType() ? 0 : 1);
connect(uiState(), &UIState::primeTypeChanged, [=](int prime_type) {

@ -44,6 +44,29 @@ MapWindow::MapWindow(const QMapboxGLSettings &settings) : m_settings(settings),
map_eta->move(25, 1080 - h - bdr_s*2);
map_eta->setVisible(false);
// Settings button
QSize icon_size(120, 120);
directions_icon = loadPixmap("../assets/navigation/icon_directions_outlined.svg", icon_size);
settings_icon = loadPixmap("../assets/navigation/icon_settings.svg", icon_size);
settings_btn = new QPushButton(directions_icon, "", this);
settings_btn->setIconSize(icon_size);
settings_btn->setStyleSheet(R"(
QPushButton {
background-color: #96000000;
border-radius: 50px;
padding: 24px;
}
QPushButton:pressed {
background-color: #D9000000;
}
)");
settings_btn->show(); // force update
settings_btn->move(bdr_s, 1080 - bdr_s*3 - settings_btn->height());
QObject::connect(settings_btn, &QPushButton::clicked, [=]() {
emit openSettings();
});
auto last_gps_position = coordinate_from_param("LastGPSPosition");
if (last_gps_position.has_value()) {
last_position = *last_gps_position;
@ -128,7 +151,7 @@ void MapWindow::updateState(const UIState &s) {
// Only open the map on setting destination the first time
if (allow_open) {
setVisible(true); // Show map on destination set/change
emit requestVisible(true); // Show map on destination set/change
allow_open = false;
}
}
@ -186,6 +209,19 @@ void MapWindow::updateState(const UIState &s) {
} else {
clearRoute();
}
// TODO: only move if position should change
// don't move while map isn't visible
if (isVisible()) {
auto pos = 1080 - bdr_s*2 - settings_btn->height() - bdr_s;
if (map_eta->isVisible()) {
settings_btn->move(bdr_s, pos - map_eta->height());
settings_btn->setIcon(settings_icon);
} else {
settings_btn->move(bdr_s, pos);
settings_btn->setIcon(directions_icon);
}
}
}
if (sm.rcv_frame("navRoute") != route_rcv_frame) {
@ -321,7 +357,7 @@ void MapWindow::offroadTransition(bool offroad) {
clearRoute();
} else {
auto dest = coordinate_from_param("NavDestination");
setVisible(dest.has_value());
emit requestVisible(dest.has_value());
}
last_bearing = {};
}

@ -11,6 +11,7 @@
#include <QMouseEvent>
#include <QOpenGLWidget>
#include <QPixmap>
#include <QPushButton>
#include <QScopedPointer>
#include <QString>
#include <QVBoxLayout>
@ -110,6 +111,8 @@ private:
MapInstructions* map_instructions;
MapETA* map_eta;
QPushButton *settings_btn;
QPixmap directions_icon, settings_icon;
void clearRoute();
void updateDestinationMarker();
@ -125,5 +128,7 @@ signals:
void distanceChanged(float distance);
void instructionsChanged(cereal::NavInstruction::Reader instruction);
void ETAChanged(float seconds, float seconds_typical, float distance);
};
void requestVisible(bool visible);
void openSettings();
};

@ -0,0 +1,34 @@
#include "selfdrive/ui/qt/maps/map_panel.h"
#include <QHBoxLayout>
#include <QWidget>
#include "selfdrive/ui/qt/maps/map.h"
#include "selfdrive/ui/qt/maps/map_settings.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/ui.h"
MapPanel::MapPanel(const QMapboxGLSettings &mapboxSettings, QWidget *parent) : QFrame(parent) {
content_stack = new QStackedLayout(this);
content_stack->setContentsMargins(0, 0, 0, 0);
auto map = new MapWindow(mapboxSettings);
QObject::connect(uiState(), &UIState::offroadTransition, map, &MapWindow::offroadTransition);
QObject::connect(map, &MapWindow::requestVisible, [=](bool visible) {
setVisible(visible);
});
QObject::connect(map, &MapWindow::openSettings, [=]() {
content_stack->setCurrentIndex(1);
});
content_stack->addWidget(map);
auto settings = new MapSettings(true, parent);
QObject::connect(settings, &MapSettings::closeSettings, [=]() {
content_stack->setCurrentIndex(0);
});
content_stack->addWidget(settings);
}
bool MapPanel::isShowingMap() const {
return content_stack->currentIndex() == 0;
}

@ -0,0 +1,17 @@
#pragma once
#include <QFrame>
#include <QMapboxGL>
#include <QStackedLayout>
class MapPanel : public QFrame {
Q_OBJECT
public:
explicit MapPanel(const QMapboxGLSettings &settings, QWidget *parent = nullptr);
bool isShowingMap() const;
private:
QStackedLayout *content_stack;
};

@ -1,132 +1,95 @@
#include "map_settings.h"
#include <QDebug>
#include <vector>
#include "common/util.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/request_repeater.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
static QString shorten(const QString &str, int max_len) {
return str.size() > max_len ? str.left(max_len).trimmed() + "" : str;
}
MapPanel::MapPanel(QWidget* parent) : QWidget(parent) {
QStackedLayout *stack = new QStackedLayout(this);
MapSettings::MapSettings(bool closeable, QWidget *parent)
: QFrame(parent), current_destination(nullptr) {
QSize icon_size(100, 100);
close_icon = loadPixmap("../assets/icons/close.svg", icon_size);
QWidget *main_widget = new QWidget;
QVBoxLayout *main_layout = new QVBoxLayout(main_widget);
main_layout->setSpacing(20);
setContentsMargins(0, 0, 0, 0);
// Home & Work layout
QHBoxLayout *home_work_layout = new QHBoxLayout;
{
// Home
home_button = new QPushButton;
home_button->setIconSize(QSize(MAP_PANEL_ICON_SIZE, MAP_PANEL_ICON_SIZE));
home_address = new QLabel;
home_address->setWordWrap(true);
QHBoxLayout *home_layout = new QHBoxLayout;
home_layout->addWidget(home_button);
home_layout->addSpacing(30);
home_layout->addWidget(home_address);
home_layout->addStretch();
// Work
work_button = new QPushButton;
work_button->setIconSize(QSize(MAP_PANEL_ICON_SIZE, MAP_PANEL_ICON_SIZE));
work_address = new QLabel;
work_address->setWordWrap(true);
QHBoxLayout *work_layout = new QHBoxLayout;
work_layout->addWidget(work_button);
work_layout->addSpacing(30);
work_layout->addWidget(work_address);
work_layout->addStretch();
home_work_layout->addLayout(home_layout, 1);
home_work_layout->addSpacing(50);
home_work_layout->addLayout(work_layout, 1);
}
auto *frame = new QVBoxLayout(this);
frame->setContentsMargins(40, 40, 40, 0);
frame->setSpacing(0);
main_layout->addLayout(home_work_layout);
main_layout->addWidget(horizontal_line());
// Current route
auto *heading_frame = new QHBoxLayout;
heading_frame->setContentsMargins(0, 0, 0, 0);
heading_frame->setSpacing(32);
{
current_widget = new QWidget(this);
QVBoxLayout *current_layout = new QVBoxLayout(current_widget);
QLabel *title = new QLabel(tr("Current Destination"));
title->setStyleSheet("font-size: 55px");
current_layout->addWidget(title);
current_route = new ButtonControl("", tr("CLEAR"));
current_route->setStyleSheet("padding-left: 40px;");
current_layout->addWidget(current_route);
QObject::connect(current_route, &ButtonControl::clicked, [=]() {
params.remove("NavDestination");
updateCurrentRoute();
});
current_layout->addSpacing(10);
current_layout->addWidget(horizontal_line());
current_layout->addSpacing(20);
}
main_layout->addWidget(current_widget);
// Recents
QLabel *recents_title = new QLabel(tr("Recent Destinations"));
recents_title->setStyleSheet("font-size: 55px");
main_layout->addWidget(recents_title);
if (closeable) {
auto *close_btn = new QPushButton("");
close_btn->setStyleSheet(R"(
QPushButton {
color: #FFFFFF;
font-size: 100px;
padding-bottom: 8px;
border 1px grey solid;
border-radius: 70px;
background-color: #292929;
font-weight: 500;
}
QPushButton:pressed {
background-color: #3B3B3B;
}
)");
close_btn->setFixedSize(140, 140);
QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closeSettings(); });
// TODO: read map_on_left from ui state
heading_frame->addWidget(close_btn);
}
recent_layout = new QVBoxLayout;
QWidget *recent_widget = new LayoutWidget(recent_layout, this);
ScrollView *recent_scroller = new ScrollView(recent_widget, this);
main_layout->addWidget(recent_scroller);
auto *heading = new QVBoxLayout;
heading->setContentsMargins(0, 0, 0, 0);
heading->setSpacing(16);
{
auto *title = new QLabel(tr("NAVIGATION"), this);
title->setStyleSheet("color: #FFFFFF; font-size: 54px; font-weight: 600;");
heading->addWidget(title);
// No prime upsell
QWidget * no_prime_widget = new QWidget;
{
QVBoxLayout *no_prime_layout = new QVBoxLayout(no_prime_widget);
QLabel *signup_header = new QLabel(tr("Try the Navigation Beta"));
signup_header->setStyleSheet(R"(font-size: 75px; color: white; font-weight:600;)");
signup_header->setAlignment(Qt::AlignCenter);
no_prime_layout->addWidget(signup_header);
no_prime_layout->addSpacing(50);
QLabel *screenshot = new QLabel;
QPixmap pm = QPixmap("../assets/navigation/screenshot.png");
screenshot->setPixmap(pm.scaledToWidth(1080, Qt::SmoothTransformation));
no_prime_layout->addWidget(screenshot, 0, Qt::AlignHCenter);
QLabel *signup = new QLabel(tr("Get turn-by-turn directions displayed and more with a comma\nprime subscription. Sign up now: https://connect.comma.ai"));
signup->setStyleSheet(R"(font-size: 45px; color: white; font-weight:300;)");
signup->setAlignment(Qt::AlignCenter);
no_prime_layout->addSpacing(20);
no_prime_layout->addWidget(signup);
no_prime_layout->addStretch();
auto *subtitle = new QLabel(tr("Manage at connect.comma.ai"), this);
subtitle->setStyleSheet("color: #A0A0A0; font-size: 40px; font-weight: 300;");
heading->addWidget(subtitle);
}
heading_frame->addLayout(heading, 1);
}
stack->addWidget(main_widget);
stack->addWidget(no_prime_widget);
connect(uiState(), &UIState::primeTypeChanged, [=](int prime_type) {
stack->setCurrentIndex(prime_type ? 0 : 1);
frame->addLayout(heading_frame);
frame->addSpacing(32);
current_widget = new DestinationWidget(this);
QObject::connect(current_widget, &DestinationWidget::actionClicked, [=]() {
if (!current_destination) return;
params.remove("NavDestination");
updateCurrentRoute();
});
frame->addWidget(current_widget);
frame->addSpacing(32);
frame->addWidget(horizontal_line());
QWidget *destinations_container = new QWidget(this);
destinations_layout = new QVBoxLayout(destinations_container);
destinations_layout->setContentsMargins(0, 32, 0, 32);
destinations_layout->setSpacing(20);
ScrollView *destinations_scroller = new ScrollView(destinations_container, this);
frame->addWidget(destinations_scroller);
clear();
setStyleSheet("MapSettings { background-color: #333333; }");
if (auto dongle_id = getDongleId()) {
// Fetch favorite and recent locations
{
QString url = CommaApi::BASE_URL + "/v1/navigation/" + *dongle_id + "/locations";
RequestRepeater* repeater = new RequestRepeater(this, url, "ApiCache_NavDestinations", 30, true);
QObject::connect(repeater, &RequestRepeater::requestDone, this, &MapPanel::parseResponse);
QObject::connect(repeater, &RequestRepeater::requestDone, this, &MapSettings::parseResponse);
}
// Destination set while offline
@ -147,153 +110,236 @@ MapPanel::MapPanel(QWidget* parent) : QWidget(parent) {
// Send DELETE to clear destination server side
deleter->sendRequest(url, HttpRequest::Method::DELETE);
}
// Update UI (athena can set destination at any time)
updateCurrentRoute();
});
}
}
}
void MapPanel::showEvent(QShowEvent *event) {
void MapSettings::showEvent(QShowEvent *event) {
updateCurrentRoute();
refresh();
}
void MapPanel::clear() {
home_button->setIcon(QPixmap("../assets/navigation/home_inactive.png"));
home_address->setStyleSheet(R"(font-size: 50px; color: grey;)");
home_address->setText(tr("No home\nlocation set"));
home_button->disconnect();
work_button->setIcon(QPixmap("../assets/navigation/work_inactive.png"));
work_address->setStyleSheet(R"(font-size: 50px; color: grey;)");
work_address->setText(tr("No work\nlocation set"));
work_button->disconnect();
clearLayout(recent_layout);
}
void MapPanel::updateCurrentRoute() {
void MapSettings::updateCurrentRoute() {
auto dest = QString::fromStdString(params.get("NavDestination"));
QJsonDocument doc = QJsonDocument::fromJson(dest.trimmed().toUtf8());
if (dest.size() && !doc.isNull()) {
auto name = doc["place_name"].toString();
auto details = doc["place_details"].toString();
current_route->setTitle(shorten(name + " " + details, 42));
if (dest.size()) {
QJsonDocument doc = QJsonDocument::fromJson(dest.trimmed().toUtf8());
if (doc.isNull()) {
qWarning() << "JSON Parse failed on NavDestination" << dest;
return;
}
auto destination = new NavDestination(doc.object());
if (current_destination && *destination == *current_destination) return;
current_destination = destination;
current_widget->set(current_destination, true);
} else {
current_destination = nullptr;
current_widget->unset("", true);
}
current_widget->setVisible(dest.size() && !doc.isNull());
if (isVisible()) refresh();
}
void MapPanel::parseResponse(const QString &response, bool success) {
if (!success) return;
void MapSettings::parseResponse(const QString &response, bool success) {
if (!success || response == cur_destinations) return;
cur_destinations = response;
if (isVisible()) {
refresh();
}
refresh();
}
void MapPanel::refresh() {
if (cur_destinations == prev_destinations) return;
QJsonDocument doc = QJsonDocument::fromJson(cur_destinations.trimmed().toUtf8());
if (doc.isNull()) {
qDebug() << "JSON Parse failed on navigation locations";
return;
}
void MapSettings::refresh() {
bool has_home = false, has_work = false;
auto destinations = std::vector<NavDestination*>();
prev_destinations = cur_destinations;
clear();
// add favorites before recents
bool has_recents = false;
for (auto &save_type: {NAV_TYPE_FAVORITE, NAV_TYPE_RECENT}) {
for (auto location : doc.array()) {
auto obj = location.toObject();
auto type = obj["save_type"].toString();
auto label = obj["label"].toString();
auto name = obj["place_name"].toString();
auto details = obj["place_details"].toString();
if (type != save_type) continue;
if (type == NAV_TYPE_FAVORITE && label == NAV_FAVORITE_LABEL_HOME) {
home_address->setText(name);
home_address->setStyleSheet(R"(font-size: 50px; color: white;)");
home_button->setIcon(QPixmap("../assets/navigation/home.png"));
QObject::connect(home_button, &QPushButton::clicked, [=]() {
navigateTo(obj);
emit closeSettings();
});
} else if (type == NAV_TYPE_FAVORITE && label == NAV_FAVORITE_LABEL_WORK) {
work_address->setText(name);
work_address->setStyleSheet(R"(font-size: 50px; color: white;)");
work_button->setIcon(QPixmap("../assets/navigation/work.png"));
QObject::connect(work_button, &QPushButton::clicked, [=]() {
navigateTo(obj);
emit closeSettings();
});
} else {
ClickableWidget *widget = new ClickableWidget;
QHBoxLayout *layout = new QHBoxLayout(widget);
layout->setContentsMargins(15, 14, 40, 14);
QLabel *star = new QLabel("");
auto sp = star->sizePolicy();
sp.setRetainSizeWhenHidden(true);
star->setSizePolicy(sp);
star->setVisible(type == NAV_TYPE_FAVORITE);
star->setStyleSheet(R"(font-size: 60px;)");
layout->addWidget(star);
layout->addSpacing(10);
QLabel *recent_label = new QLabel(shorten(name + " " + details, 45));
recent_label->setStyleSheet(R"(font-size: 50px;)");
layout->addWidget(recent_label);
layout->addStretch();
QLabel *arrow = new QLabel("");
arrow->setStyleSheet(R"(font-size: 60px;)");
layout->addWidget(arrow);
widget->setStyleSheet(R"(
.ClickableWidget {
border-radius: 10px;
border-width: 1px;
border-style: solid;
border-color: gray;
}
QWidget {
background-color: #393939;
color: #9c9c9c;
}
)");
auto destinations_str = cur_destinations.trimmed();
if (!destinations_str.isEmpty()) {
QJsonDocument doc = QJsonDocument::fromJson(destinations_str.toUtf8());
if (doc.isNull()) {
qWarning() << "JSON Parse failed on navigation locations" << cur_destinations;
return;
}
QObject::connect(widget, &ClickableWidget::clicked, [=]() {
navigateTo(obj);
emit closeSettings();
});
for (auto el : doc.array()) {
auto destination = new NavDestination(el.toObject());
recent_layout->addWidget(widget);
recent_layout->addSpacing(10);
has_recents = true;
// add home and work later if they are missing
if (destination->isFavorite()) {
if (destination->label() == NAV_FAVORITE_LABEL_HOME) has_home = true;
else if (destination->label() == NAV_FAVORITE_LABEL_WORK) has_work = true;
}
// skip current destination
if (current_destination && *destination == *current_destination) continue;
destinations.push_back(destination);
}
}
if (!has_recents) {
QLabel *no_recents = new QLabel(tr("no recent destinations"));
no_recents->setStyleSheet(R"(font-size: 50px; color: #9c9c9c)");
recent_layout->addWidget(no_recents);
// TODO: should we build a new layout and swap it in?
clearLayout(destinations_layout);
// Sort: HOME, WORK, and then descending-alphabetical FAVORITES, RECENTS
std::sort(destinations.begin(), destinations.end(), [](const NavDestination *a, const NavDestination *b) {
if (a->isFavorite() && b->isFavorite()) {
if (a->label() == NAV_FAVORITE_LABEL_HOME) return true;
else if (b->label() == NAV_FAVORITE_LABEL_HOME) return false;
else if (a->label() == NAV_FAVORITE_LABEL_WORK) return true;
else if (b->label() == NAV_FAVORITE_LABEL_WORK) return false;
else if (a->label() != b->label()) return a->label() < b->label();
}
else if (a->isFavorite()) return true;
else if (b->isFavorite()) return false;
return a->name() < b->name();
});
for (auto destination : destinations) {
auto widget = new DestinationWidget(this);
widget->set(destination, false);
QObject::connect(widget, &QPushButton::clicked, [=]() {
navigateTo(destination->toJson());
emit closeSettings();
});
destinations_layout->addWidget(widget);
}
recent_layout->addStretch();
// add home and work if missing
if (!has_home) {
auto widget = new DestinationWidget(this);
widget->unset(NAV_FAVORITE_LABEL_HOME);
destinations_layout->insertWidget(0, widget);
}
if (!has_work) {
auto widget = new DestinationWidget(this);
widget->unset(NAV_FAVORITE_LABEL_WORK);
// TODO: refactor to remove this hack
int index = !has_home || (current_destination && current_destination->isFavorite() && current_destination->label() == NAV_FAVORITE_LABEL_HOME) ? 0 : 1;
destinations_layout->insertWidget(index, widget);
}
destinations_layout->addStretch();
repaint();
}
void MapPanel::navigateTo(const QJsonObject &place) {
void MapSettings::navigateTo(const QJsonObject &place) {
QJsonDocument doc(place);
params.put("NavDestination", doc.toJson().toStdString());
updateCurrentRoute();
}
DestinationWidget::DestinationWidget(QWidget *parent) : QPushButton(parent) {
setContentsMargins(0, 0, 0, 0);
auto *frame = new QHBoxLayout(this);
frame->setContentsMargins(32, 24, 32, 24);
frame->setSpacing(32);
icon = new QLabel(this);
icon->setAlignment(Qt::AlignCenter);
icon->setFixedSize(96, 96);
icon->setObjectName("icon");
frame->addWidget(icon);
auto *inner_frame = new QVBoxLayout;
inner_frame->setContentsMargins(0, 0, 0, 0);
inner_frame->setSpacing(0);
{
title = new ElidedLabel(this);
title->setAttribute(Qt::WA_TransparentForMouseEvents);
inner_frame->addWidget(title);
subtitle = new ElidedLabel(this);
subtitle->setAttribute(Qt::WA_TransparentForMouseEvents);
subtitle->setObjectName("subtitle");
inner_frame->addWidget(subtitle);
}
frame->addLayout(inner_frame, 1);
action = new QPushButton(this);
action->setFixedSize(96, 96);
action->setObjectName("action");
action->setStyleSheet("font-size: 65px; font-weight: 600;");
QObject::connect(action, &QPushButton::clicked, [=]() { emit clicked(); });
QObject::connect(action, &QPushButton::clicked, [=]() { emit actionClicked(); });
frame->addWidget(action);
setFixedHeight(164);
setStyleSheet(R"(
DestinationWidget { background-color: #202123; border-radius: 10px; }
QLabel { color: #FFFFFF; font-size: 48px; font-weight: 400; }
#icon { background-color: #3B4356; border-radius: 48px; }
#subtitle { color: #9BA0A5; }
#action { border: none; border-radius: 48px; color: #FFFFFF; padding-bottom: 4px; }
/* current destination */
[current="true"] { background-color: #E8E8E8; }
[current="true"] QLabel { color: #000000; }
[current="true"] #icon { background-color: #42906B; }
[current="true"] #subtitle { color: #333333; }
[current="true"] #action { color: #202123; }
/* no saved destination */
[set="false"] QLabel { color: #9BA0A5; }
[current="true"][set="false"] QLabel { color: #A0000000; }
/* pressed */
[current="false"]:pressed { background-color: #18191B; }
[current="true"] #action:pressed { background-color: #D6D6D6; }
)");
}
void DestinationWidget::set(NavDestination *destination, bool current) {
setProperty("current", current);
setProperty("set", true);
auto icon_pixmap = current ? icons().directions : icons().recent;
auto title_text = destination->name();
auto subtitle_text = destination->details();
if (destination->isFavorite()) {
if (destination->label() == NAV_FAVORITE_LABEL_HOME) {
icon_pixmap = icons().home;
title_text = tr("Home");
subtitle_text = destination->name() + ", " + destination->details();
} else if (destination->label() == NAV_FAVORITE_LABEL_WORK) {
icon_pixmap = icons().work;
title_text = tr("Work");
subtitle_text = destination->name() + ", " + destination->details();
} else {
icon_pixmap = icons().favorite;
}
}
icon->setPixmap(icon_pixmap);
// TODO: onroad and offroad have different dimensions
title->setText(shorten(title_text, 26));
subtitle->setText(shorten(subtitle_text, 26));
subtitle->setVisible(true);
// TODO: use pixmap
action->setAttribute(Qt::WA_TransparentForMouseEvents, !current);
action->setText(current ? "×" : "");
action->setVisible(true);
setStyleSheet(styleSheet());
}
void DestinationWidget::unset(const QString &label, bool current) {
setProperty("current", current);
setProperty("set", false);
if (label.isEmpty()) {
icon->setPixmap(icons().directions);
title->setText(tr("No destination set"));
} else {
QString title_text = label == NAV_FAVORITE_LABEL_HOME ? tr("home") : tr("work");
icon->setPixmap(label == NAV_FAVORITE_LABEL_HOME ? icons().home : icons().work);
title->setText(tr("No %1 location set").arg(title_text));
}
subtitle->setVisible(false);
action->setVisible(false);
setStyleSheet(styleSheet());
}

@ -1,46 +1,120 @@
#pragma once
#include <QFrame>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include <QStackedWidget>
#include "common/params.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/controls.h"
const int MAP_PANEL_ICON_SIZE = 200;
const QString NAV_TYPE_FAVORITE = "favorite";
const QString NAV_TYPE_RECENT = "recent";
const QString NAV_FAVORITE_LABEL_HOME = "home";
const QString NAV_FAVORITE_LABEL_WORK = "work";
class MapPanel : public QWidget {
class NavDestination;
class DestinationWidget;
class MapSettings : public QFrame {
Q_OBJECT
public:
explicit MapPanel(QWidget* parent = nullptr);
explicit MapSettings(bool closeable = false, QWidget *parent = nullptr);
void navigateTo(const QJsonObject &place);
void parseResponse(const QString &response, bool success);
void updateCurrentRoute();
void clear();
private:
void showEvent(QShowEvent *event) override;
void refresh();
Params params;
QString prev_destinations, cur_destinations;
QPushButton *home_button, *work_button;
QLabel *home_address, *work_address;
QVBoxLayout *recent_layout;
QWidget *current_widget;
ButtonControl *current_route;
QString cur_destinations;
QVBoxLayout *destinations_layout;
NavDestination *current_destination;
DestinationWidget *current_widget;
QPixmap close_icon;
signals:
void closeSettings();
};
class NavDestination {
public:
explicit NavDestination(const QJsonObject &place)
: type_(place["save_type"].toString()), label_(place["label"].toString()),
name_(place["place_name"].toString()), details_(place["place_details"].toString()),
latitude_(place["latitude"].toDouble()), longitude_(place["longitude"].toDouble()) {
// if details starts with `name, ` remove it
if (details_.startsWith(name_ + ", ")) {
details_ = details_.mid(name_.length() + 2);
}
}
QString type() const { return type_; }
QString label() const { return label_; }
QString name() const { return name_; }
QString details() const { return details_; }
bool isFavorite() const { return type_ == NAV_TYPE_FAVORITE; }
bool isRecent() const { return type_ == NAV_TYPE_RECENT; }
bool operator==(const NavDestination &rhs) {
return type_ == rhs.type_ && label_ == rhs.label_ && name_ == rhs.name_ &&
details_ == rhs.details_ && latitude_ == rhs.latitude_ && longitude_ == rhs.longitude_;
}
QJsonObject toJson() const {
QJsonObject obj;
obj["save_type"] = type_;
obj["label"] = label_;
obj["place_name"] = name_;
obj["place_details"] = details_;
obj["latitude"] = latitude_;
obj["longitude"] = longitude_;
return obj;
}
private:
QString type_, label_, name_, details_;
double latitude_, longitude_;
};
class DestinationWidget : public QPushButton {
Q_OBJECT
public:
explicit DestinationWidget(QWidget *parent = nullptr);
void set(NavDestination *, bool current = false);
void unset(const QString &label, bool current = false);
signals:
void actionClicked();
private:
struct NavIcons {
QPixmap home, work, favorite, recent, directions;
};
static NavIcons icons() {
static NavIcons nav_icons {
loadPixmap("../assets/navigation/icon_home.svg", {48, 48}),
loadPixmap("../assets/navigation/icon_work.svg", {48, 48}),
loadPixmap("../assets/navigation/icon_favorite.svg", {48, 48}),
loadPixmap("../assets/navigation/icon_recent.svg", {48, 48}),
loadPixmap("../assets/navigation/icon_directions.svg", {48, 48}),
};
return nav_icons;
}
private:
QLabel *icon, *title, *subtitle;
QPushButton *action;
};

@ -8,10 +8,6 @@
#include "selfdrive/ui/qt/offroad/networking.h"
#ifdef ENABLE_MAPS
#include "selfdrive/ui/qt/maps/map_settings.h"
#endif
#include "common/params.h"
#include "common/watchdog.h"
#include "common/util.h"
@ -385,12 +381,6 @@ SettingsWindow::SettingsWindow(QWidget *parent) : QFrame(parent) {
{tr("Software"), new SoftwarePanel(this)},
};
#ifdef ENABLE_MAPS
auto map_panel = new MapPanel(this);
panels.push_back({tr("Navigation"), map_panel});
QObject::connect(map_panel, &MapPanel::closeSettings, this, &SettingsWindow::closeSettings);
#endif
nav_btns = new QButtonGroup(this);
for (auto &[name, panel] : panels) {
QPushButton *btn = new QPushButton(name);

@ -3,12 +3,13 @@
#include <cmath>
#include <QDebug>
#include <QMouseEvent>
#include "common/timing.h"
#include "selfdrive/ui/qt/util.h"
#ifdef ENABLE_MAPS
#include "selfdrive/ui/qt/maps/map.h"
#include "selfdrive/ui/qt/maps/map_helpers.h"
#include "selfdrive/ui/qt/maps/map_panel.h"
#endif
OnroadWindow::OnroadWindow(QWidget *parent) : QWidget(parent) {
@ -78,10 +79,15 @@ void OnroadWindow::updateState(const UIState &s) {
}
void OnroadWindow::mousePressEvent(QMouseEvent* e) {
#ifdef ENABLE_MAPS
if (map != nullptr) {
bool sidebarVisible = geometry().x() > 0;
if (map->isVisible() && !((MapPanel *)map)->isShowingMap() && e->windowPos().x() >= 1080) {
return;
}
map->setVisible(!sidebarVisible && !map->isVisible());
}
#endif
// propagation event to parent(HomeWindow)
QWidget::mousePressEvent(e);
}
@ -90,16 +96,14 @@ void OnroadWindow::offroadTransition(bool offroad) {
#ifdef ENABLE_MAPS
if (!offroad) {
if (map == nullptr && (uiState()->primeType() || !MAPBOX_TOKEN.isEmpty())) {
MapWindow * m = new MapWindow(get_mapbox_settings());
auto m = new MapPanel(get_mapbox_settings());
map = m;
QObject::connect(uiState(), &UIState::offroadTransition, m, &MapWindow::offroadTransition);
m->setFixedWidth(topWidget(this)->width() / 2 - bdr_s);
split->insertWidget(0, m);
// Make map visible after adding to split
m->offroadTransition(offroad);
// hidden by default, made visible when navRoute is published
m->setVisible(false);
}
}
#endif

@ -7,8 +7,6 @@ QFrame *horizontal_line(QWidget *parent) {
QFrame *line = new QFrame(parent);
line->setFrameShape(QFrame::StyledPanel);
line->setStyleSheet(R"(
margin-left: 40px;
margin-right: 40px;
border-width: 1px;
border-bottom-style: solid;
border-color: gray;
@ -127,19 +125,3 @@ void ElidedLabel::paintEvent(QPaintEvent *event) {
opt.initFrom(this);
style()->drawItemText(&painter, contentsRect(), alignment(), opt.palette, isEnabled(), elidedText_, foregroundRole());
}
ClickableWidget::ClickableWidget(QWidget *parent) : QWidget(parent) { }
void ClickableWidget::mouseReleaseEvent(QMouseEvent *event) {
if (rect().contains(event->pos())) {
emit clicked();
}
}
// Fix stylesheets
void ClickableWidget::paintEvent(QPaintEvent *) {
QStyleOption opt;
opt.init(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

@ -298,17 +298,3 @@ public:
setLayout(l);
}
};
class ClickableWidget : public QWidget {
Q_OBJECT
public:
ClickableWidget(QWidget *parent = nullptr);
protected:
void mouseReleaseEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *) override;
signals:
void clicked();
};

@ -116,6 +116,33 @@
<translation>Ablehnen, deinstallieren %1</translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -356,42 +383,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation>Aktuelles Ziel</translation>
</message>
<message>
<source>CLEAR</source>
<translation>LÖSCHEN</translation>
</message>
<message>
<source>Recent Destinations</source>
<translation>Letzte Ziele</translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation>Beta Navigation ausprobieren</translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation>Erhalte echtzeit Wegführung und mehr mit dem comma prime
Abonnement. Melde dich jetzt an: https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation>Keine Heimadresse gesetzt</translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation>Keine Arbeitsadresse gesetzt</translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation>Keine kürzlich gewählten Ziele</translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -676,10 +675,6 @@ This may take up to a minute.</source>
<source>Software</source>
<translation>Software</translation>
</message>
<message>
<source>Navigation</source>
<translation>Navigation</translation>
</message>
</context>
<context>
<name>Setup</name>

@ -116,6 +116,33 @@
<translation> %1 </translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -355,44 +382,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation></translation>
</message>
<message>
<source>CLEAR</source>
<translation></translation>
</message>
<message>
<source>Recent Destinations</source>
<translation></translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation>β</translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation>
https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation>
</translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation>
</translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation></translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -674,10 +671,6 @@ This may take up to a minute.</source>
<source>Software</source>
<translation>ソ</translation>
</message>
<message>
<source>Navigation</source>
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>

@ -116,6 +116,33 @@
<translation>, %1 </translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -355,44 +382,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation> </translation>
</message>
<message>
<source>CLEAR</source>
<translation></translation>
</message>
<message>
<source>Recent Destinations</source>
<translation> </translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation>() </translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation> comma prime을 .
https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation>
</translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation>
</translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation> </translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -675,10 +672,6 @@ This may take up to a minute.</source>
<source>Software</source>
<translation></translation>
</message>
<message>
<source>Navigation</source>
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>

@ -116,6 +116,33 @@
<translation>Rejeitar, desintalar %1</translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -356,44 +383,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation>Destino Atual</translation>
</message>
<message>
<source>CLEAR</source>
<translation>LIMPAR</translation>
</message>
<message>
<source>Recent Destinations</source>
<translation>Destinos Recentes</translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation>Experimente a Navegação Beta</translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation>Obtenha instruções passo a passo exibidas e muito mais com
uma assinatura prime. Inscreva-se agora: https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation>Sem local
residência definido</translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation>Sem local de
trabalho definido</translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation>sem destinos recentes</translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -679,10 +676,6 @@ Isso pode levar até um minuto.</translation>
<source>Software</source>
<translation>Software</translation>
</message>
<message>
<source>Navigation</source>
<translation>Navegação</translation>
</message>
</context>
<context>
<name>Setup</name>

@ -116,6 +116,33 @@
<translation>%1</translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -355,42 +382,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation></translation>
</message>
<message>
<source>CLEAR</source>
<translation></translation>
</message>
<message>
<source>Recent Destinations</source>
<translation></translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation></translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation>comma prime以获取导航
https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation></translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation></translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation></translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -672,10 +671,6 @@ This may take up to a minute.</source>
<source>Software</source>
<translation></translation>
</message>
<message>
<source>Navigation</source>
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>

@ -116,6 +116,33 @@
<translation> %1</translation>
</message>
</context>
<context>
<name>DestinationWidget</name>
<message>
<source>Home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Work</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No destination set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No %1 location set</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>home</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>work</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>DevicePanel</name>
<message>
@ -355,44 +382,14 @@
</message>
</context>
<context>
<name>MapPanel</name>
<message>
<source>Current Destination</source>
<translation></translation>
</message>
<message>
<source>CLEAR</source>
<translation></translation>
</message>
<message>
<source>Recent Destinations</source>
<translation></translation>
</message>
<message>
<source>Try the Navigation Beta</source>
<translation></translation>
</message>
<message>
<source>Get turn-by-turn directions displayed and more with a comma
prime subscription. Sign up now: https://connect.comma.ai</source>
<translation> comma 使
https://connect.comma.ai</translation>
</message>
<message>
<source>No home
location set</source>
<translation>
</translation>
</message>
<name>MapSettings</name>
<message>
<source>No work
location set</source>
<translation>
</translation>
<source>NAVIGATION</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>no recent destinations</source>
<translation></translation>
<source>Manage at connect.comma.ai</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
@ -674,10 +671,6 @@ This may take up to a minute.</source>
<source>Software</source>
<translation></translation>
</message>
<message>
<source>Navigation</source>
<translation></translation>
</message>
</context>
<context>
<name>Setup</name>

Loading…
Cancel
Save