diff --git a/selfdrive/assets/navigation/icon_directions.svg b/selfdrive/assets/navigation/icon_directions.svg new file mode 100644 index 0000000000..c36e27316e --- /dev/null +++ b/selfdrive/assets/navigation/icon_directions.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:935cd01600d56350cda1941cf382c7c86cd959fa7a0a574bdba8b3011a350578 +size 466 diff --git a/selfdrive/assets/navigation/icon_directions_outlined.svg b/selfdrive/assets/navigation/icon_directions_outlined.svg new file mode 100644 index 0000000000..058cf98122 --- /dev/null +++ b/selfdrive/assets/navigation/icon_directions_outlined.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3284def3e6e8b2683b7a13eaaa86b9555a8cce2ff6e7144ce9392c451c77c3d7 +size 757 diff --git a/selfdrive/assets/navigation/icon_favorite.svg b/selfdrive/assets/navigation/icon_favorite.svg new file mode 100644 index 0000000000..0eb0e43688 --- /dev/null +++ b/selfdrive/assets/navigation/icon_favorite.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff184bf0239c54060ffbaf465573f17cbc92bd8d3d38ce10e1bd02cdd2b21575 +size 315 diff --git a/selfdrive/assets/navigation/icon_home.svg b/selfdrive/assets/navigation/icon_home.svg new file mode 100644 index 0000000000..ca90cc7bf6 --- /dev/null +++ b/selfdrive/assets/navigation/icon_home.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1673f8d46251a05787b60346193852991739345506dc7e9b106dfb370d3611ed +size 489 diff --git a/selfdrive/assets/navigation/icon_recent.svg b/selfdrive/assets/navigation/icon_recent.svg new file mode 100644 index 0000000000..76b5a620b1 --- /dev/null +++ b/selfdrive/assets/navigation/icon_recent.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fbce167d2005d08e8bc2113e0a9d5d3e2ed113db8e2a020df1ee14633cd3eed +size 1279 diff --git a/selfdrive/assets/navigation/icon_settings.svg b/selfdrive/assets/navigation/icon_settings.svg new file mode 100644 index 0000000000..3fd31459c3 --- /dev/null +++ b/selfdrive/assets/navigation/icon_settings.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fa72d94b5e05884129dd502babd5de78666c349bb5d12e19872fdbc9fe2100e +size 910 diff --git a/selfdrive/assets/navigation/icon_work.svg b/selfdrive/assets/navigation/icon_work.svg new file mode 100644 index 0000000000..dc18914a39 --- /dev/null +++ b/selfdrive/assets/navigation/icon_work.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ea949d3b66896ac3404d20aea9445a5cd00fefd18623f1c7160293a1de5f807 +size 364 diff --git a/selfdrive/assets/navigation/screenshot.png b/selfdrive/assets/navigation/screenshot.png deleted file mode 100644 index 53c374d791..0000000000 --- a/selfdrive/assets/navigation/screenshot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bdddd78a5899f475f0fdddf9ecef9a3d13027d0d1162b493baa9b4299a8f7064 -size 1461575 diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index f46e3d5873..97d4060767 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -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) diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index 587e2f445e..85c6c34267 100644 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -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) { diff --git a/selfdrive/ui/qt/maps/map.cc b/selfdrive/ui/qt/maps/map.cc index abbf434d69..bcf6e9d062 100644 --- a/selfdrive/ui/qt/maps/map.cc +++ b/selfdrive/ui/qt/maps/map.cc @@ -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 = {}; } diff --git a/selfdrive/ui/qt/maps/map.h b/selfdrive/ui/qt/maps/map.h index 0d8b93a5f4..658d26ee47 100644 --- a/selfdrive/ui/qt/maps/map.h +++ b/selfdrive/ui/qt/maps/map.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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(); +}; diff --git a/selfdrive/ui/qt/maps/map_panel.cc b/selfdrive/ui/qt/maps/map_panel.cc new file mode 100644 index 0000000000..564012482a --- /dev/null +++ b/selfdrive/ui/qt/maps/map_panel.cc @@ -0,0 +1,34 @@ +#include "selfdrive/ui/qt/maps/map_panel.h" + +#include +#include + +#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; +} diff --git a/selfdrive/ui/qt/maps/map_panel.h b/selfdrive/ui/qt/maps/map_panel.h new file mode 100644 index 0000000000..abea77483a --- /dev/null +++ b/selfdrive/ui/qt/maps/map_panel.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include + +class MapPanel : public QFrame { + Q_OBJECT + +public: + explicit MapPanel(const QMapboxGLSettings &settings, QWidget *parent = nullptr); + + bool isShowingMap() const; + +private: + QStackedLayout *content_stack; +}; diff --git a/selfdrive/ui/qt/maps/map_settings.cc b/selfdrive/ui/qt/maps/map_settings.cc index f626925ad4..e9c8dcfcbd 100644 --- a/selfdrive/ui/qt/maps/map_settings.cc +++ b/selfdrive/ui/qt/maps/map_settings.cc @@ -1,132 +1,95 @@ #include "map_settings.h" #include +#include #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(); - 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()); } diff --git a/selfdrive/ui/qt/maps/map_settings.h b/selfdrive/ui/qt/maps/map_settings.h index 8dd044c374..38c021036c 100644 --- a/selfdrive/ui/qt/maps/map_settings.h +++ b/selfdrive/ui/qt/maps/map_settings.h @@ -1,46 +1,120 @@ #pragma once + +#include #include #include #include #include #include #include -#include -#include #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; +}; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index d9f200865f..83ab772115 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -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); diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index c678d07faf..e9009d42b0 100644 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -3,12 +3,13 @@ #include #include +#include #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 diff --git a/selfdrive/ui/qt/widgets/controls.cc b/selfdrive/ui/qt/widgets/controls.cc index 456cf748f4..6693c2247b 100644 --- a/selfdrive/ui/qt/widgets/controls.cc +++ b/selfdrive/ui/qt/widgets/controls.cc @@ -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); -} diff --git a/selfdrive/ui/qt/widgets/controls.h b/selfdrive/ui/qt/widgets/controls.h index 807441cc85..9e745ad1af 100644 --- a/selfdrive/ui/qt/widgets/controls.h +++ b/selfdrive/ui/qt/widgets/controls.h @@ -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(); -}; diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 18ce67ee96..d67def18e2 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -116,6 +116,33 @@ Ablehnen, deinstallieren %1 + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -356,42 +383,14 @@ - MapPanel - - Current Destination - Aktuelles Ziel - - - CLEAR - LÖSCHEN - - - Recent Destinations - Letzte Ziele - - - Try the Navigation Beta - Beta Navigation ausprobieren - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - Erhalte echtzeit Wegführung und mehr mit dem comma prime -Abonnement. Melde dich jetzt an: https://connect.comma.ai - - - No home -location set - Keine Heimadresse gesetzt - + MapSettings - No work -location set - Keine Arbeitsadresse gesetzt + NAVIGATION + - no recent destinations - Keine kürzlich gewählten Ziele + Manage at connect.comma.ai + @@ -676,10 +675,6 @@ This may take up to a minute. Software Software - - Navigation - Navigation - Setup diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index ad430615de..8cae4dec1f 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -116,6 +116,33 @@ 拒否して %1 をアンインストール + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -355,44 +382,14 @@ - MapPanel - - Current Destination - 現在の目的地 - - - CLEAR - 削除 - - - Recent Destinations - 最近の目的地 - - - Try the Navigation Beta - β版ナビゲーションを試す - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - より詳細な案内情報を得ることができます。 -詳しくはこちら:https://connect.comma.ai - - - No home -location set - 自宅の住所はまだ -設定されていません - + MapSettings - No work -location set - 職場の住所はまだ -設定されていません + NAVIGATION + - no recent destinations - 最近の目的地履歴がありません + Manage at connect.comma.ai + @@ -674,10 +671,6 @@ This may take up to a minute. Software ソフトウェア - - Navigation - ナビゲーション - Setup diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 3e427779e1..e024a39998 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -116,6 +116,33 @@ 거절, %1 제거 + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -355,44 +382,14 @@ - MapPanel - - Current Destination - 현재 목적지 - - - CLEAR - 삭제 - - - Recent Destinations - 최근 목적지 - - - Try the Navigation Beta - 네비게이션(베타)를 사용해보세요 - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - 자세한 경로안내를 원하시면 comma prime을 구독하세요. -등록:https://connect.comma.ai - - - No home -location set - 집 -설정되지않음 - + MapSettings - No work -location set - 회사 -설정되지않음 + NAVIGATION + - no recent destinations - 최근 목적지 없음 + Manage at connect.comma.ai + @@ -675,10 +672,6 @@ This may take up to a minute. Software 소프트웨어 - - Navigation - 네비게이션 - Setup diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index d7ca16c2c8..4235571dcd 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -116,6 +116,33 @@ Rejeitar, desintalar %1 + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -356,44 +383,14 @@ - MapPanel - - Current Destination - Destino Atual - - - CLEAR - LIMPAR - - - Recent Destinations - Destinos Recentes - - - Try the Navigation Beta - Experimente a Navegação Beta - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - Obtenha instruções passo a passo exibidas e muito mais com -uma assinatura prime. Inscreva-se agora: https://connect.comma.ai - - - No home -location set - Sem local -residência definido - + MapSettings - No work -location set - Sem local de -trabalho definido + NAVIGATION + - no recent destinations - sem destinos recentes + Manage at connect.comma.ai + @@ -679,10 +676,6 @@ Isso pode levar até um minuto. Software Software - - Navigation - Navegação - Setup diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index c7ddca8998..7ff99d1824 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -116,6 +116,33 @@ 拒绝并卸载%1 + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -355,42 +382,14 @@ - MapPanel - - Current Destination - 当前目的地 - - - CLEAR - 清空 - - - Recent Destinations - 最近目的地 - - - Try the Navigation Beta - 试用导航测试版 - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - 订阅comma prime以获取导航。 -立即注册:https://connect.comma.ai - - - No home -location set - 家:未设定 - + MapSettings - No work -location set - 工作:未设定 + NAVIGATION + - no recent destinations - 无最近目的地 + Manage at connect.comma.ai + @@ -672,10 +671,6 @@ This may take up to a minute. Software 软件 - - Navigation - 导航 - Setup diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 06eca034f2..748a4f5943 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -116,6 +116,33 @@ 拒絕並解除安裝 %1 + + DestinationWidget + + Home + + + + Work + + + + No destination set + + + + No %1 location set + + + + home + + + + work + + + DevicePanel @@ -355,44 +382,14 @@ - MapPanel - - Current Destination - 當前目的地 - - - CLEAR - 清除 - - - Recent Destinations - 最近目的地 - - - Try the Navigation Beta - 試用導航功能 - - - Get turn-by-turn directions displayed and more with a comma -prime subscription. Sign up now: https://connect.comma.ai - 成為 comma 高級會員來使用導航功能 -立即註冊:https://connect.comma.ai - - - No home -location set - 未設定 -住家位置 - + MapSettings - No work -location set - 未設定 -工作位置 + NAVIGATION + - no recent destinations - 沒有最近的導航記錄 + Manage at connect.comma.ai + @@ -674,10 +671,6 @@ This may take up to a minute. Software 軟體 - - Navigation - 導航 - Setup