diff --git a/src/fw/apps/system/quick_launch_nothing.c b/src/fw/apps/system/quick_launch_nothing.c new file mode 100644 index 0000000000..c63dca294c --- /dev/null +++ b/src/fw/apps/system/quick_launch_nothing.c @@ -0,0 +1,26 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +//! The "Nothing" Quick Launch action. Binding a button to it makes the button +//! inert: the shell recognizes the app id and never launches anything. + +#include "quick_launch_nothing.h" + +#include "pbl/services/i18n/i18n.h" + +static void prv_main(void) { + // Never runs; the shell drops the launch before it gets here. +} + +const PebbleProcessMd *quick_launch_nothing_get_app_info(void) { + static const PebbleProcessMdSystem s_app_info = { + .common = { + .main_func = &prv_main, + .uuid = QUICK_LAUNCH_NOTHING_UUID, + .visibility = ProcessVisibilityQuickLaunch, + }, + /// Quick Launch action that makes the button do nothing at all. + .name = i18n_noop("Nothing"), + }; + return &s_app_info.common; +} diff --git a/src/fw/apps/system/quick_launch_nothing.h b/src/fw/apps/system/quick_launch_nothing.h new file mode 100644 index 0000000000..b8628a952e --- /dev/null +++ b/src/fw/apps/system/quick_launch_nothing.h @@ -0,0 +1,11 @@ +/* SPDX-FileCopyrightText: 2026 Core Devices LLC */ +/* SPDX-License-Identifier: Apache-2.0 */ + +#pragma once + +#include "process_management/app_manager.h" + +#define QUICK_LAUNCH_NOTHING_UUID {0xde, 0x6d, 0xa1, 0x7f, 0x1a, 0x10, 0x47, 0x25, \ + 0xad, 0xbb, 0x1e, 0xfc, 0x22, 0xe4, 0x3f, 0x04} + +const PebbleProcessMd *quick_launch_nothing_get_app_info(void); diff --git a/src/fw/apps/system/settings/quick_launch.c b/src/fw/apps/system/settings/quick_launch.c index aa1d1f5d33..6f8f22f594 100644 --- a/src/fw/apps/system/settings/quick_launch.c +++ b/src/fw/apps/system/settings/quick_launch.c @@ -2,12 +2,13 @@ /* SPDX-License-Identifier: Apache-2.0 */ //! This file displays the main Quick Launch menu that is found in our settings menu -//! It allows the feature to be enabled or for an app to be set -//! The list of apps that the user can choose from is found in settings_quick_launch_app_menu.c +//! It allows the feature to be enabled or for an action or app to be set +//! The list of targets that the user can choose from is found in quick_launch_app_menu.c //! This file is also responsible for saving / storing the uuid of each quichlaunch app as well as //! whether or not the quicklaunch app is enabled. #include "menu.h" +#include "option_menu.h" #include "quick_launch.h" #include "quick_launch_app_menu.h" #include "quick_launch_setup_menu.h" @@ -18,6 +19,7 @@ #include "applib/app_launch_reason.h" #include "applib/ui/window_stack.h" #include "kernel/pbl_malloc.h" +#include "process_management/app_install_manager.h" #include "process_management/app_menu_data_source.h" #include "resource/resource_ids.auto.h" #include "pbl/services/i18n/i18n.h" @@ -36,9 +38,19 @@ typedef enum { ROW_HOLD_BACK, } QuickLaunchRow; +typedef enum { + CATEGORY_ROW_ACTIONS = 0, + CATEGORY_ROW_APPS, + + CategoryRowCount, +} QuickLaunchCategoryRow; + typedef struct QuickLaunchData { SettingsCallbacks callbacks; char app_names[NUM_ROWS][APP_NAME_SIZE_BYTES]; + //! The button whose binding the pushed sub-menus are editing. + ButtonId button; + bool is_tap; } QuickLaunchData; static const char *s_row_titles[NUM_ROWS] = { @@ -56,11 +68,18 @@ static const char *s_row_titles[NUM_ROWS] = { [ROW_HOLD_BACK] = i18n_noop("Hold Back"), }; +static const char *s_category_row_titles[CategoryRowCount] = { + /// Shown in Quick Launch Settings as the title of the list of actions. + [CATEGORY_ROW_ACTIONS] = i18n_noop("Actions"), + /// Shown in Quick Launch Settings as the title of the list of apps. + [CATEGORY_ROW_APPS] = i18n_noop("Apps"), +}; + static void prv_get_subtitle_string(AppInstallId app_id, QuickLaunchData *data, char *buffer, uint8_t buf_len) { if (app_id == INSTALL_ID_INVALID) { - /// Shown in Quick Launch Settings when the button is disabled. - i18n_get_with_buffer("Disabled", buffer, buf_len); + /// Shown in Quick Launch Settings when no action or app is bound to the button. + i18n_get_with_buffer("Unassigned", buffer, buf_len); return; } else { AppInstallEntry entry; @@ -125,12 +144,40 @@ static uint16_t prv_get_initial_selection_cb(SettingsCallbacks *context) { return 0; } +//! The row to start on, which is the category the button is currently bound to. +static QuickLaunchCategoryRow prv_get_category_row(QuickLaunchData *data) { + const AppInstallId app_id = data->is_tap ? quick_launch_single_click_get_app(data->button) + : quick_launch_get_app(data->button); + AppInstallEntry entry; + if ((app_id == INSTALL_ID_INVALID) || !app_install_get_entry_for_install_id(app_id, &entry)) { + return CATEGORY_ROW_ACTIONS; + } + return app_install_entry_is_quick_launch_visible_only(&entry) ? CATEGORY_ROW_ACTIONS + : CATEGORY_ROW_APPS; +} + +static void prv_category_menu_select(OptionMenu *option_menu, int selection, void *context) { + QuickLaunchData *data = settings_option_menu_get_context(context); + const QuickLaunchMenuCategory category = (selection == CATEGORY_ROW_ACTIONS) ? + QuickLaunchMenuCategoryActions : QuickLaunchMenuCategoryApps; + quick_launch_app_menu_window_push(data->button, data->is_tap, category, &option_menu->window); +} + +static void prv_category_menu_push(QuickLaunchData *data) { + const OptionMenuCallbacks callbacks = { + .select = prv_category_menu_select, + }; + settings_option_menu_push(i18n_noop("Quick Launch"), OptionMenuContentType_SingleLine, + prv_get_category_row(data), &callbacks, CategoryRowCount, + false /* icons_enabled */, s_category_row_titles, data); +} + static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) { PBL_ASSERTN(row < NUM_ROWS); - + ButtonId button; bool is_tap; - + switch (row) { case ROW_TAP_UP: button = BUTTON_ID_UP; @@ -159,8 +206,11 @@ static void prv_select_click_cb(SettingsCallbacks *context, uint16_t row) { default: return; } - - quick_launch_app_menu_window_push(button, is_tap); + + QuickLaunchData *data = (QuickLaunchData *)context; + data->button = button; + data->is_tap = is_tap; + prv_category_menu_push(data); } static uint16_t prv_num_rows_cb(SettingsCallbacks *context) { diff --git a/src/fw/apps/system/settings/quick_launch_app_menu.c b/src/fw/apps/system/settings/quick_launch_app_menu.c index b1980007a8..afdb884ba1 100644 --- a/src/fw/apps/system/settings/quick_launch_app_menu.c +++ b/src/fw/apps/system/settings/quick_launch_app_menu.c @@ -1,9 +1,8 @@ /* SPDX-FileCopyrightText: 2024 Google LLC */ /* SPDX-License-Identifier: Apache-2.0 */ -//! This file generates a menu that lets the user select a quicklaunch app -//! The menu that is generated is the same as the "main menu" but with a -//! title +//! This file generates the menu that lets the user pick a Quick Launch target +//! within one category, either an action or an app. #include "quick_launch_app_menu.h" #include "quick_launch_setup_menu.h" @@ -28,12 +27,32 @@ typedef struct { AppMenuDataSource data_source; ButtonId button; bool is_tap; - int16_t selected; + QuickLaunchMenuCategory category; + Window *parent; OptionMenu *option_menu; } QuickLaunchAppMenuData; +//! Row 0 of both categories un-assigns the button; the data source starts after it. #define NUM_CUSTOM_CELLS 1 +static const char *s_category_titles[] = { + /// Title of the Quick Launch menu listing the available actions. + [QuickLaunchMenuCategoryActions] = i18n_noop("Actions"), + /// Title of the Quick Launch menu listing the installed apps. + [QuickLaunchMenuCategoryApps] = i18n_noop("Apps"), +}; + +//! Row 0 when nothing is bound, the matching entry when it is in this category, and no +//! selection at all when the button is bound to something in the other category. +static int prv_choice(AppInstallId install_id, uint16_t app_index) { + if (install_id == INSTALL_ID_INVALID) { + return 0; + } + if (app_index == MENU_INDEX_NOT_FOUND) { + return OPTION_MENU_CHOICE_NONE; + } + return (int)app_index + NUM_CUSTOM_CELLS; +} /* Callback Functions */ @@ -42,15 +61,21 @@ static bool prv_app_filter_callback(struct AppMenuDataSource *source, AppInstall const Uuid timeline_future_uuid = TIMELINE_UUID_INIT; const Uuid timeline_past_uuid = TIMELINE_PAST_UUID_INIT; const Uuid timeline_full_uuid = TIMELINE_FULL_UUID_INIT; - + if (app_install_entry_is_watchface(entry)) { return false; // Skip watchfaces } - if (app_install_entry_is_hidden(entry) && - !app_install_entry_is_quick_launch_visible_only(entry)) { - return false; // Skip hidden apps unless they are quick launch visible + + // Quick launch visible only entries are the actions, everything else is an app. + const bool is_action = app_install_entry_is_quick_launch_visible_only(entry); + if (data->category == QuickLaunchMenuCategoryActions) { + if (!is_action) { + return false; + } + } else if (is_action || app_install_entry_is_hidden(entry)) { + return false; } - + // For tap buttons, filter Timeline apps based on button if (data->is_tap) { if (data->button == BUTTON_ID_UP) { @@ -71,7 +96,7 @@ static bool prv_app_filter_callback(struct AppMenuDataSource *source, AppInstall } } } - + return true; } @@ -84,9 +109,10 @@ static void prv_menu_draw_row(OptionMenu *option_menu, GContext* ctx, const Laye const GRect *text_frame, uint32_t row, bool selected, void *context) { QuickLaunchAppMenuData *data = context; - const char *text = NULL; + const char *text; if (row == 0) { - text = i18n_get("Disable", data); + /// Shown in Quick Launch Settings when no action or app is bound to the button. + text = i18n_get("Unassigned", data); } else { AppMenuNode *node = app_menu_data_source_get_node_at_index(&data->data_source, row - NUM_CUSTOM_CELLS); @@ -107,17 +133,18 @@ static void prv_menu_select(OptionMenu *option_menu, int selection, void *contex quick_launch_set_app(data->button, INSTALL_ID_INVALID); quick_launch_set_enabled(data->button, false); } - app_window_stack_pop(true); } else { - AppMenuNode* app_menu_node = + AppMenuNode *app_menu_node = app_menu_data_source_get_node_at_index(&data->data_source, selection - NUM_CUSTOM_CELLS); if (data->is_tap) { quick_launch_single_click_set_app(data->button, app_menu_node->install_id); } else { quick_launch_set_app(data->button, app_menu_node->install_id); } - app_window_stack_pop(true); } + // Unwind the category menu too, so that selecting returns to the Quick Launch settings. + app_window_stack_remove(data->parent, false); + app_window_stack_pop(true); } static void prv_menu_reload_data(void *context) { @@ -134,10 +161,13 @@ static void prv_menu_unload(OptionMenu *option_menu, void *context) { app_free(data); } -void quick_launch_app_menu_window_push(ButtonId button, bool is_tap) { +void quick_launch_app_menu_window_push(ButtonId button, bool is_tap, + QuickLaunchMenuCategory category, Window *parent) { QuickLaunchAppMenuData *data = app_zalloc_check(sizeof(*data)); data->button = button; data->is_tap = is_tap; + data->category = category; + data->parent = parent; OptionMenu *option_menu = option_menu_create(); data->option_menu = option_menu; @@ -149,13 +179,13 @@ void quick_launch_app_menu_window_push(ButtonId button, bool is_tap) { const AppInstallId install_id = is_tap ? quick_launch_single_click_get_app(button) : quick_launch_get_app(button); - const int app_index = app_menu_data_source_get_index_of_app_with_install_id(&data->data_source, - install_id); + const uint16_t app_index = + app_menu_data_source_get_index_of_app_with_install_id(&data->data_source, install_id); GColor highlight_bg = shell_prefs_get_theme_highlight_color(); const OptionMenuConfig config = { - .title = i18n_get(i18n_noop("Quick Launch"), data), - .choice = (install_id == INSTALL_ID_INVALID) ? 0 : (app_index + NUM_CUSTOM_CELLS), + .title = i18n_get(s_category_titles[category], data), + .choice = prv_choice(install_id, app_index), .status_colors = { GColorWhite, GColorBlack, }, .highlight_colors = { highlight_bg, gcolor_legible_over(highlight_bg) }, .icons_enabled = true, diff --git a/src/fw/apps/system/settings/quick_launch_app_menu.h b/src/fw/apps/system/settings/quick_launch_app_menu.h index 090c872a6c..b541bd179a 100644 --- a/src/fw/apps/system/settings/quick_launch_app_menu.h +++ b/src/fw/apps/system/settings/quick_launch_app_menu.h @@ -4,6 +4,18 @@ #pragma once #include "shell/normal/quick_launch.h" + +#include "applib/ui/window.h" + #include -void quick_launch_app_menu_window_push(ButtonId button, bool is_tap); +typedef enum QuickLaunchMenuCategory { + //! Entries that are only visible in Quick Launch, e.g. the system toggles. + QuickLaunchMenuCategoryActions, + //! Regular launcher apps. + QuickLaunchMenuCategoryApps, +} QuickLaunchMenuCategory; + +//! @param parent The category menu to unwind along with this one on selection. +void quick_launch_app_menu_window_push(ButtonId button, bool is_tap, + QuickLaunchMenuCategory category, Window *parent); diff --git a/src/fw/apps/system/toggle/airplane_mode.c b/src/fw/apps/system/toggle/airplane_mode.c index cf7abdc63f..c9fa5ce61c 100644 --- a/src/fw/apps/system/toggle/airplane_mode.c +++ b/src/fw/apps/system/toggle/airplane_mode.c @@ -48,7 +48,8 @@ const PebbleProcessMd *airplane_mode_toggle_get_app_info(void) { .uuid = AIRPLANE_MODE_TOGGLE_UUID, .visibility = ProcessVisibilityQuickLaunch, }, - .name = i18n_noop("Airplane Mode"), + /// The Quick Launch action that toggles Airplane Mode. + .name = i18n_noop("Toggle Airplane Mode"), }; return &s_app_info.common; } diff --git a/src/fw/apps/system/toggle/backlight_state.c b/src/fw/apps/system/toggle/backlight_state.c index 31ddcabd42..d82fac4734 100644 --- a/src/fw/apps/system/toggle/backlight_state.c +++ b/src/fw/apps/system/toggle/backlight_state.c @@ -49,7 +49,8 @@ const PebbleProcessMd *backlight_state_toggle_get_app_info(void) { .uuid = BACKLIGHT_STATE_TOGGLE_UUID, .visibility = ProcessVisibilityQuickLaunch, }, - .name = i18n_noop("Backlight"), + /// The Quick Launch action that toggles the backlight. + .name = i18n_noop("Toggle Backlight"), }; return &s_app_info.common; } diff --git a/src/fw/apps/system/toggle/motion_backlight.c b/src/fw/apps/system/toggle/motion_backlight.c index 142cc60eaa..79c440f787 100644 --- a/src/fw/apps/system/toggle/motion_backlight.c +++ b/src/fw/apps/system/toggle/motion_backlight.c @@ -46,7 +46,8 @@ const PebbleProcessMd *motion_backlight_toggle_get_app_info(void) { .uuid = MOTION_BACKLIGHT_TOGGLE_UUID, .visibility = ProcessVisibilityQuickLaunch, }, - .name = i18n_noop("Motion Backlight"), + /// The Quick Launch action that toggles the motion backlight. + .name = i18n_noop("Toggle Motion Backlight"), }; return &s_app_info.common; } diff --git a/src/fw/apps/system/toggle/quiet_time.c b/src/fw/apps/system/toggle/quiet_time.c index 9fc71561c2..935ce434ad 100644 --- a/src/fw/apps/system/toggle/quiet_time.c +++ b/src/fw/apps/system/toggle/quiet_time.c @@ -21,7 +21,8 @@ const PebbleProcessMd *quiet_time_toggle_get_app_info(void) { .uuid = QUIET_TIME_TOGGLE_UUID, .visibility = ProcessVisibilityQuickLaunch, }, - .name = i18n_noop("Quiet Time"), + /// The Quick Launch action that toggles Quiet Time. + .name = i18n_noop("Toggle Quiet Time"), }; return &s_app_info.common; } diff --git a/src/fw/shell/normal/system_app_registry_list.json b/src/fw/shell/normal/system_app_registry_list.json index 773c08ee50..8dd4bb68e2 100644 --- a/src/fw/shell/normal/system_app_registry_list.json +++ b/src/fw/shell/normal/system_app_registry_list.json @@ -673,6 +673,11 @@ "id": -100, "enum": "NOTIFICATIONS_CLEAR_HISTORY", "md_fn": "notifications_clear_history_app_get_info" + }, + { + "id": -101, + "enum": "QUICK_LAUNCH_NOTHING", + "md_fn": "quick_launch_nothing_get_app_info" } ], "resource_apps": [ diff --git a/src/fw/shell/normal/watchface.c b/src/fw/shell/normal/watchface.c index 0afacd03f2..3f473b5938 100644 --- a/src/fw/shell/normal/watchface.c +++ b/src/fw/shell/normal/watchface.c @@ -6,7 +6,6 @@ #include "apps/system_app_ids.h" #include "apps/system/launcher/launcher.h" #include "apps/system/settings/quick_launch.h" -#include "apps/system/settings/quick_launch_app_menu.h" #include "apps/system/settings/quick_launch_setup_menu.h" #include "apps/system/timeline/timeline.h" #include "apps/watch/low_power/face.h" @@ -187,6 +186,10 @@ static void prv_launch_timeline_app(AppInstallId app_id, ButtonId button, static void prv_launch_quick_launch_app(AppInstallId app_id, ButtonId button, AppLaunchReason timeline_reason, AppQuickLaunchAction action) { + if (app_id == APP_ID_QUICK_LAUNCH_NOTHING) { + return; + } + const bool is_timeline = (app_id == APP_ID_TIMELINE) || (app_id == APP_ID_TIMELINE_PAST) || (app_id == APP_ID_TIMELINE_FULL);