Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 35 additions & 5 deletions projects/APPLaunch/main/ui/esc_hold_hint_controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ bool EscHoldHintController::handle(const key_item *item)
return false;

if (item->key_state == KBD_KEY_PRESSED) {
if (!return_home_enabled_) return true;
const bool home_hold = home_hold_available();
if (!return_home_enabled_ && !home_hold) return true;
if (!model_.press(lv_tick_get())) return true;
hold_is_home_ = home_hold;
if (!poll_timer_)
poll_timer_ = lv_timer_create(poll_timer_cb, kPollPeriodMs, this);
if (poll_timer_) {
Expand Down Expand Up @@ -66,6 +68,21 @@ void EscHoldHintController::set_force_home_callback(ForceHomeCallback callback,
}
}

void EscHoldHintController::set_home_hold_callback(HomeHoldCallback callback, void *user_data)
{
home_hold_callback_ = callback;
home_hold_user_data_ = user_data;
if (!callback && hold_is_home_) {
if (poll_timer_) lv_timer_pause(poll_timer_);
if (model_.cancel()) launcher_toast().hide();
}
}

bool EscHoldHintController::home_hold_available() const
{
return !return_home_enabled_ && home_hold_callback_ != nullptr;
}

void EscHoldHintController::shutdown()
{
if (poll_timer_) {
Expand All @@ -75,18 +92,31 @@ void EscHoldHintController::shutdown()
if (model_.cancel()) launcher_toast().hide();
force_home_callback_ = nullptr;
force_home_user_data_ = nullptr;
home_hold_callback_ = nullptr;
home_hold_user_data_ = nullptr;
}

void EscHoldHintController::poll(lv_timer_t *timer)
{
if (!esc_hold_timer_is_current(timer, poll_timer_)) return;
/* A hold started on the home screen ends if an app takes over, and vice versa. */
const bool hold_still_valid = hold_is_home_ ? home_hold_available() : return_home_enabled_;
const bool callback_available =
hold_is_home_ ? home_hold_callback_ != nullptr : force_home_callback_ != nullptr;
const EscHoldPollDecision decision = model_.poll(
lv_tick_get(), cp0_esc_state_read() != 0, force_home_callback_ != nullptr,
return_home_enabled_);
lv_tick_get(), cp0_esc_state_read() != 0, callback_available, hold_still_valid);
if (decision.hide_hint) launcher_toast().hide();
if (decision.show_hint)
launcher_toast().show_persistent("Hold ESC 3s to return home");
if (decision.force_home) {
launcher_toast().show_persistent(hold_is_home_ ? "Hold ESC 3s for power menu"
: "Hold ESC 3s to return home");
if (decision.force_home && hold_is_home_) {
HomeHoldCallback callback = home_hold_callback_;
void *user_data = home_hold_user_data_;
/* The menu takes over input and swallows this ESC's release, so end the
* hold here or the next press would be ignored. */
if (model_.cancel()) launcher_toast().hide();
if (callback) callback(user_data);
} else if (decision.force_home) {
ForceHomeCallback callback = force_home_callback_;
void *user_data = force_home_user_data_;
if (callback) callback(user_data);
Expand Down
8 changes: 8 additions & 0 deletions projects/APPLaunch/main/ui/esc_hold_hint_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,30 @@ class EscHoldHintController
{
public:
using ForceHomeCallback = void (*)(void *user_data);
using HomeHoldCallback = void (*)(void *user_data);

bool handle(const key_item *item);
void clear_hint_ownership();
void set_return_home_enabled(bool enabled);
void set_force_home_callback(ForceHomeCallback callback, void *user_data);
/* Fired when ESC is held for the same 3s on the launcher home screen
* (i.e. while return-home is disabled). */
void set_home_hold_callback(HomeHoldCallback callback, void *user_data);
void shutdown();

private:
void poll(lv_timer_t *timer);
bool home_hold_available() const;
static void poll_timer_cb(lv_timer_t *timer) noexcept;

lv_timer_t *poll_timer_ = nullptr;
EscHoldLifecycleModel model_;
ForceHomeCallback force_home_callback_ = nullptr;
void *force_home_user_data_ = nullptr;
HomeHoldCallback home_hold_callback_ = nullptr;
void *home_hold_user_data_ = nullptr;
bool return_home_enabled_ = false;
bool hold_is_home_ = false;
};

EscHoldHintController &esc_hold_hint_controller();
16 changes: 16 additions & 0 deletions projects/APPLaunch/main/ui/launch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "generated/page_app.h"
#include "ui_launch_page.h"
#include "ui_screensaver.h"
#include "ui_power_menu.h"
#include "ui_loading.h"
#include "cp0_lvgl_app.h"
#include "sample_log.h"
Expand Down Expand Up @@ -49,6 +50,7 @@ void Launch::bind_ui()
app_directory_watcher_.start([this] { applications_reload(); });
esc_ui_watchdog_.start();
esc_hold_hint_controller().set_force_home_callback(esc_force_home_cb, this);
esc_hold_hint_controller().set_home_hold_callback(esc_home_hold_cb, this);
}

void Launch::launch_app()
Expand Down Expand Up @@ -274,11 +276,25 @@ void Launch::esc_force_home_cb(void *user_data) noexcept
}
}

void Launch::esc_home_hold_cb(void *user_data) noexcept
{
auto *self = static_cast<Launch *>(user_data);
if (!self || self->app_Page || LVGL_RUN_FLAGE == 0) return;
if (self->page_lifecycle_.state() != LauncherPageState::HOME) return;
if (ui_screensaver_is_active()) return;
try {
SLOGI("[HOME] ESC held for 3000 ms on home, opening power menu");
ui_power_menu::open();
} catch (...) {
}
}


Launch::~Launch()
{
launcher_app_registry_clear_changed_callback(app_registry_changed_cb, this);
esc_hold_hint_controller().set_force_home_callback(nullptr, nullptr);
esc_hold_hint_controller().set_home_hold_callback(nullptr, nullptr);
esc_hold_hint_controller().set_return_home_enabled(false);
esc_ui_watchdog_.shutdown();
app_directory_watcher_.stop();
Expand Down
1 change: 1 addition & 0 deletions projects/APPLaunch/main/ui/launch.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class Launch

static void lv_go_back_home(void *arg) noexcept;
static void esc_force_home_cb(void *user_data) noexcept;
static void esc_home_hold_cb(void *user_data) noexcept;
static void app_registry_changed_cb(void *user_data) noexcept;

std::weak_ptr<UILaunchPage> launch_page_;
Expand Down
116 changes: 116 additions & 0 deletions projects/APPLaunch/main/ui/model/power_menu_model.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/

#include "power_menu_model.hpp"

namespace {

constexpr PowerMenuItem kItems[PowerMenuModel::kItemCount] = {
PowerMenuItem::Cancel,
PowerMenuItem::Lock,
PowerMenuItem::Reboot,
PowerMenuItem::Shutdown,
};

} // namespace

const char *PowerMenuModel::label(PowerMenuItem item)
{
switch (item) {
case PowerMenuItem::Lock:
return "Lock";
case PowerMenuItem::Reboot:
return "Reboot";
case PowerMenuItem::Shutdown:
return "Shutdown";
case PowerMenuItem::Cancel:
default:
return "Cancel";
}
}

const char *PowerMenuModel::confirm_title(PowerMenuItem item)
{
switch (item) {
case PowerMenuItem::Lock:
return "Lock the screen?";
case PowerMenuItem::Reboot:
return "Reboot the system?";
case PowerMenuItem::Shutdown:
return "Shut down the system?";
case PowerMenuItem::Cancel:
default:
return "";
}
}

PowerMenuItem PowerMenuModel::item_at(size_t index)
{
return index < kItemCount ? kItems[index] : PowerMenuItem::Cancel;
}

void PowerMenuModel::open()
{
stage_ = PowerMenuStage::Choosing;
selected_ = 0; // Cancel is the default.
}

void PowerMenuModel::close()
{
stage_ = PowerMenuStage::Closed;
selected_ = 0;
}

PowerMenuDecision PowerMenuModel::handle(PowerMenuKey key)
{
PowerMenuDecision decision;
switch (stage_) {
case PowerMenuStage::Choosing:
switch (key) {
case PowerMenuKey::Previous:
selected_ = (selected_ + kItemCount - 1) % kItemCount;
decision.redraw = decision.navigated = true;
break;
case PowerMenuKey::Next:
selected_ = (selected_ + 1) % kItemCount;
decision.redraw = decision.navigated = true;
break;
case PowerMenuKey::Enter:
if (selected() == PowerMenuItem::Cancel) {
close();
decision.close = true;
} else {
stage_ = PowerMenuStage::Confirming;
decision.redraw = true;
}
break;
case PowerMenuKey::Back:
close();
decision.close = true;
break;
case PowerMenuKey::Other:
default:
break;
}
break;
case PowerMenuStage::Confirming:
if (key == PowerMenuKey::Enter) {
decision.execute = true;
decision.action = selected();
close();
decision.close = true;
} else if (key == PowerMenuKey::Back) {
// Back to the list with the same entry still highlighted.
stage_ = PowerMenuStage::Choosing;
decision.redraw = true;
}
break;
case PowerMenuStage::Closed:
default:
break;
}
return decision;
}
69 changes: 69 additions & 0 deletions projects/APPLaunch/main/ui/model/power_menu_model.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <cstddef>
#include <cstdint>

/* Home-screen power menu opened by holding ESC. Pure logic: no LVGL, so the
* navigation and confirmation rules are unit-tested in test_power_menu_model.cpp. */

enum class PowerMenuItem : uint8_t
{
Cancel,
Lock,
Reboot,
Shutdown,
};

enum class PowerMenuStage : uint8_t
{
Closed,
Choosing,
Confirming,
};

enum class PowerMenuKey : uint8_t
{
Previous,
Next,
Enter,
Back,
Other,
};

struct PowerMenuDecision
{
bool redraw = false;
bool close = false;
bool navigated = false;
bool execute = false;
PowerMenuItem action = PowerMenuItem::Cancel;
};

class PowerMenuModel
{
public:
static constexpr size_t kItemCount = 4;

static const char *label(PowerMenuItem item);
static const char *confirm_title(PowerMenuItem item);
static PowerMenuItem item_at(size_t index);

void open();
void close();
PowerMenuDecision handle(PowerMenuKey key);

PowerMenuStage stage() const { return stage_; }
bool is_open() const { return stage_ != PowerMenuStage::Closed; }
size_t selected_index() const { return selected_; }
PowerMenuItem selected() const { return item_at(selected_); }

private:
PowerMenuStage stage_ = PowerMenuStage::Closed;
size_t selected_ = 0;
};
5 changes: 5 additions & 0 deletions projects/APPLaunch/main/ui/ui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "ui_global_hint.h"
#include "ui_loading.h"
#include "ui_screensaver.h"
#include "ui_power_menu.h"
#include <stdio.h>
#include <cstdlib>
#include "lvgl/src/widgets/gif/lv_gif.h"
Expand All @@ -40,6 +41,9 @@ void init()
cp0_keyboard_set_global_key_handler(ui_global_hint::on_key);
home = std::make_unique<LauncherUiRuntime>();
home->start();
/* Before the low-battery warning, so its filter chains to this one and
* keeps priority over the menu. */
ui_power_menu::init();
launcher_battery_ui::init_warning();
}

Expand All @@ -48,6 +52,7 @@ void deinit()
cp0_keyboard_set_global_key_handler(nullptr);
ui_global_hint::shutdown();
launcher_battery_ui::shutdown_warning();
ui_power_menu::shutdown();
ui_loading::shutdown();
ui_screensaver_deinit();
home.reset();
Expand Down
Loading
Loading