UI: improved nav destination entry (#28476)

pull/28642/head
Cameron Clough 2 years ago committed by GitHub
parent 2b800cf2e7
commit bc5a899024
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      selfdrive/assets/navigation/icon_directions.svg
  2. 1
      selfdrive/assets/navigation/icon_directions_outlined.svg
  3. 1
      selfdrive/assets/navigation/icon_favorite.svg
  4. 1
      selfdrive/assets/navigation/icon_home.svg
  5. 1
      selfdrive/assets/navigation/icon_recent.svg
  6. 1
      selfdrive/assets/navigation/icon_settings.svg
  7. 1
      selfdrive/assets/navigation/icon_work.svg
  8. BIN
      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 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g fill="#fff" clip-path="url(#a)" transform="matrix(2.3325 0 0 2.3825 -72.89 -63.797)"><path d="M31.25 53.12A15.62 15.62 0 0 1 46.88 37.5h9.37v-9.16a1.56 1.56 0 0 1 2.56-1.2l14.75 12.28c.75.63.75 1.78 0 2.4l-14.75 12.3a1.56 1.56 0 0 1-2.56-1.2v-9.17h-9.38a9.37 9.37 0 0 0-9.37 9.37v15.63h-6.25z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 466 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g fill="#fff" clip-path="url(#a)"><path d="M31.25 53.12A15.62 15.62 0 0 1 46.88 37.5h9.37v-9.16a1.56 1.56 0 0 1 2.56-1.2l14.75 12.28c.75.63.75 1.78 0 2.4l-14.75 12.3a1.56 1.56 0 0 1-2.56-1.2v-9.17h-9.38a9.37 9.37 0 0 0-9.37 9.37v15.63h-6.25V53.12Z"/><path fill-rule="evenodd" d="M43.44 2.72a9.28 9.28 0 0 1 13.12 0l40.72 40.72a9.26 9.26 0 0 1 0 13.12L56.56 97.28a9.26 9.26 0 0 1-13.1 0L2.71 56.56a9.26 9.26 0 0 1 0-13.11L43.44 2.72Zm8.75 4.37a3.1 3.1 0 0 0-4.38 0L7.1 47.81a3.1 3.1 0 0 0 0 4.38L47.8 92.9a3.1 3.1 0 0 0 4.38 0L92.9 52.2a3.1 3.1 0 0 0 0-4.38L52.2 7.1Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 757 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g clip-path="url(#a)"><path fill="#fff" d="M49.1 0 36.35 31.82H0l27.27 27.27L13.64 100l35.45-23.64L86.36 100 72.73 59.1 100 31.81H63.64L49.09 0Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 315 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g clip-path="url(#a)"><path fill="#fff" d="M7.7 98.08c0 1.06.85 1.92 1.92 1.92h28.84V76.92h23.08V100H90.4c1.06 0 1.92-.86 1.92-1.92V53.85H7.69v44.23ZM99.47 42.9l-7.17-6.3V7.7a7.7 7.7 0 0 0-15.39 0v15.4L51.4.68A1.92 1.92 0 0 0 48.68.6l-.08.08L.52 42.72a1.92 1.92 0 0 0 1.38 3.24l13.49.2h82.68a1.92 1.92 0 0 0 1.41-3.25Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 489 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g fill="#fff" clip-path="url(#a)"><path d="M53.22 6.37a43.75 43.75 0 0 0-3.21-.12V0a50 50 0 0 1 3.68.14l-.47 6.23Zm12.53 2.81c-2-.77-4.06-1.4-6.16-1.87l1.37-6.1c2.4.54 4.75 1.25 7.04 2.14l-2.25 5.83Zm8.56 4.44c-.9-.6-1.8-1.16-2.74-1.69l3.08-5.44a50.15 50.15 0 0 1 6.12 4.09l-3.85 4.93a43.75 43.75 0 0 0-2.61-1.89ZM85.77 24.8a43.7 43.7 0 0 0-4.08-4.97l4.53-4.31a52.14 52.14 0 0 1 4.66 5.68l-5.1 3.6Zm4.65 8.45a36.5 36.5 0 0 0-1.34-2.92l5.59-2.81a48.6 48.6 0 0 1 2.8 6.8l-5.93 1.95a43.9 43.9 0 0 0-1.12-3.02Zm3.31 15.67a43.68 43.68 0 0 0-.62-6.4l6.15-1.07c.42 2.41.67 4.86.73 7.31l-6.25.16Zm-.81 9.61c.2-1.06.37-2.12.5-3.19l6.2.77a49.3 49.3 0 0 1-1.43 7.22l-6.02-1.67c.28-1.03.53-2.07.75-3.13ZM86.97 73.4A44.16 44.16 0 0 0 90 67.72l5.72 2.53c-1 2.25-2.16 4.42-3.47 6.49l-5.28-3.34Zm-6.03 7.53c.76-.77 1.5-1.55 2.19-2.36l4.74 4.08a49.2 49.2 0 0 1-2.51 2.7l-4.42-4.42Z"/><path d="M50 6.25a43.75 43.75 0 1 0 30.94 74.68l4.42 4.42A50 50 0 1 1 50 0v6.25Z"/><path d="M46.88 18.75a3.13 3.13 0 0 1 3.13 3.12v32.56l20.3 11.6a3.12 3.12 0 0 1-3.1 5.42l-21.88-12.5a3.12 3.12 0 0 1-1.57-2.7V21.86a3.12 3.12 0 0 1 3.12-3.12Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" fill="#fff" class="bi bi-gear-fill"><path d="M75.24 8.4c-3.304-11.2-19.176-11.2-22.48 0l-.8 2.72a11.712 11.712 0 0 1-16.84 6.976l-2.48-1.36c-10.264-5.584-21.488 5.64-15.896 15.896l1.352 2.488c3.568 6.56.184 14.728-6.976 16.84l-2.72.8c-11.2 3.304-11.2 19.176 0 22.48l2.72.8a11.712 11.712 0 0 1 6.976 16.84l-1.36 2.48c-5.584 10.264 5.64 21.488 15.896 15.896l2.488-1.352a11.712 11.712 0 0 1 16.84 6.976l.8 2.72c3.304 11.2 19.176 11.2 22.48 0l.8-2.72a11.712 11.712 0 0 1 16.84-6.976l2.48 1.36c10.264 5.584 21.488-5.64 15.896-15.896l-1.352-2.488a11.712 11.712 0 0 1 6.976-16.84l2.72-.8c11.2-3.304 11.2-19.176 0-22.48l-2.72-.8a11.712 11.712 0 0 1-6.976-16.84l1.36-2.48c5.584-10.264-5.64-21.488-15.896-15.896l-2.488 1.352a11.712 11.712 0 0 1-16.84-6.976ZM64 87.44a23.44 23.44 0 1 1 0-46.88 23.432 23.432 0 0 1 0 46.864z" style="stroke-width:8"/></svg>

After

Width:  |  Height:  |  Size: 910 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" fill="none"><g clip-path="url(#a)"><path fill="#fff" d="M77.78 22.22V6.42L70.67 0H29.33l-7.1 8.3v13.92H5.55L0 27.78v66.66L5.56 100h88.88l5.56-5.56V27.78l-5.56-5.56H77.78Zm-44.45-11.1h33.34v11.1H33.33v-11.1Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h100v100H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

@ -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