/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "action_menu_window.h" #include "action_menu_window_private.h" #include "applib/applib_malloc.auto.h" #include "applib/ui/status_bar_layer.h" #include "applib/ui/window.h" #include "applib/ui/window_stack.h" #include "process_state/app_state/app_state.h" #include "services/normal/timeline/timeline.h" #include "services/common/i18n/i18n.h" #define ACTION_MENU_DEFAULT_BACKGROUND_COLOR GColorWhite static const int IN_OUT_ANIMATION_DURATION = 200; static void prv_invoke_will_close(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); if (data->config.will_close) { data->config.will_close(action_menu, data->performed_item, data->config.context); } } static void prv_invoke_did_close(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); if (data->config.did_close) { data->config.did_close(action_menu, data->performed_item, data->config.context); } } static void prv_action_window_push(WindowStack *window_stack, ActionMenu *action_menu, bool animated) { window_stack_push(window_stack, &action_menu->window, animated); } static void prv_action_window_pop(bool animated, ActionMenu *action_menu) { prv_invoke_will_close(action_menu); window_stack_remove(&action_menu->window, animated); } static void prv_action_window_insert_below(ActionMenu *action_menu, Window *window) { window_stack_insert_next(action_menu->window.parent_window_stack, window); } static void prv_remove_window(Window *window) { window_stack_remove(window, false /* animated */); } static void prv_view_model_did_change(ActionMenuData *data) { ActionMenuViewModel *vm = &data->view_model; const ActionMenuLevel *cur_level = vm->cur_level; GRect frame = grect_inset(data->action_menu.window.layer.frame, vm->menu_insets); layer_set_frame(&data->action_menu_layer.layer, &frame); if (cur_level->display_mode == ActionMenuLevelDisplayModeThin) { action_menu_layer_set_items(&data->action_menu_layer, NULL, 0, 0, 0); action_menu_layer_set_short_items(&data->action_menu_layer, cur_level->items, cur_level->num_items, cur_level->default_selected_item); } else { action_menu_layer_set_short_items(&data->action_menu_layer, NULL, 0, 0); action_menu_layer_set_items(&data->action_menu_layer, cur_level->items, cur_level->num_items, cur_level->default_selected_item, cur_level->separator_index); } crumbs_layer_set_level(&data->crumbs_layer, vm->num_dots); } static void prv_next_level_anim_stopped(Animation *anim, bool finished, void *context) { // update the view model AnimationContext *anim_ctx = (AnimationContext*) context; ActionMenuData *data = window_get_user_data((Window*) anim_ctx->window); if (!data || !finished) { // We could have gotten cleaned up in the middle of an animation, bail applib_free(anim_ctx); return; } if (data->view_model.cur_level->parent_level == anim_ctx->next_level) { --data->view_model.num_dots; } else { ++data->view_model.num_dots; } data->view_model.cur_level = anim_ctx->next_level; // update the view prv_view_model_did_change(data); applib_free(anim_ctx); } static GEdgeInsets prv_action_menu_insets(Window *window) { const int crumbs_width = crumbs_layer_width(); return (GEdgeInsets) { .top = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT), .right = PBL_IF_RECT_ELSE(0, crumbs_width), .bottom = PBL_IF_RECT_ELSE(0, STATUS_BAR_LAYER_HEIGHT), .left = crumbs_width, }; } static Animation* prv_create_content_in_animation(ActionMenuData *data, const ActionMenuLevel *level) { // animate the ease in of the new level const GRect window_frame = data->action_menu.window.layer.frame; const GEdgeInsets insets = prv_action_menu_insets(&data->action_menu.window); GRect stop = grect_inset(window_frame, insets); GRect start = stop; start.origin.x -= crumbs_layer_width(); PropertyAnimation *prop_anim = property_animation_create_layer_frame((Layer *)&data->action_menu_layer, &start, &stop); Animation *content_in = property_animation_get_animation(prop_anim); animation_set_duration(content_in, IN_OUT_ANIMATION_DURATION); #if !defined(PLATFORM_TINTIN) // animate the dots Animation *crumbs_anim = crumbs_layer_get_animation(&data->crumbs_layer); animation_set_duration(crumbs_anim, IN_OUT_ANIMATION_DURATION); // combine the two Animation *spawn_anim = animation_spawn_create(content_in, crumbs_anim, NULL); return spawn_anim; #else return content_in; #endif } static Animation* prv_create_content_out_animation(ActionMenuData *data, const ActionMenuLevel *level) { // animate the ease out of the current level GRect *start = &data->action_menu_layer.layer.frame; GRect stop = *start; stop.origin.x = crumbs_layer_width() - start->size.w; PropertyAnimation *prop_anim = property_animation_create_layer_frame((Layer *)&data->action_menu_layer, start, &stop); Animation *content_out = property_animation_get_animation(prop_anim); animation_set_duration(content_out, IN_OUT_ANIMATION_DURATION); AnimationHandlers anim_handlers = { .started = NULL, .stopped = prv_next_level_anim_stopped, }; AnimationContext *anim_ctx = applib_type_malloc(AnimationContext); *anim_ctx = (AnimationContext) { .window = &data->action_menu.window, .next_level = level, }; animation_set_handlers(content_out, anim_handlers, anim_ctx); return content_out; } static void prv_set_level(ActionMenuData *data, const ActionMenuLevel *level) { if (animation_is_scheduled(data->level_change_anim)) { // We are already animating. return; } Animation *content_out = prv_create_content_out_animation(data, level); Animation *content_in = prv_create_content_in_animation(data, level); data->level_change_anim = animation_sequence_create(content_out, content_in, NULL); animation_schedule(data->level_change_anim); } static void prv_action_callback(const ActionMenuItem *item, void *context) { ActionMenu *action_menu = context; ActionMenuData *data = window_get_user_data(&action_menu->window); if (item->is_leaf && item->perform_action) { item->perform_action(action_menu, item, data->config.context); data->performed_item = item; if (!data->frozen) { prv_action_window_pop(true /*animated*/, action_menu); } } else if (item->next_level) { prv_set_level(data, item->next_level); } } static void prv_back_click_handler(ClickRecognizerRef recognizer, void *context) { ActionMenuData *data = context; if (animation_is_scheduled(data->level_change_anim)) { animation_set_elapsed(data->level_change_anim, animation_get_duration(data->level_change_anim, true, true)); } ActionMenuLevel *parent_level = data->view_model.cur_level->parent_level; if (parent_level) { prv_set_level(data, parent_level); } else { prv_action_window_pop(true, &data->action_menu); } } static void prv_click_config_provider(void *context) { ActionMenuData *data = context; action_menu_layer_click_config_provider(&data->action_menu_layer); window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler); window_set_click_context(BUTTON_ID_BACK, data); } static void prv_action_window_load(Window *window) { ActionMenuData *data = window_get_user_data(window); // Init action menu layer ActionMenuLayer *action_menu_layer = &data->action_menu_layer; action_menu_layer_init(action_menu_layer, &GRectZero); action_menu_layer_set_callback(action_menu_layer, prv_action_callback, (void *)window); action_menu_layer_set_align(action_menu_layer, data->config.align); // Init crumbs layer CrumbsLayer *crumbs_layer = &data->crumbs_layer; GRect frame = window_get_root_layer(window)->frame; #if PBL_RECT // on round display, the layer fills a full circle // here (on rect) it's just a small vertical stripe on the left frame.size.w = crumbs_layer_width(); #endif crumbs_layer_init(&data->crumbs_layer, &frame, data->config.colors.background, data->config.colors.foreground); // Add them to the tree layer_add_child(window_get_root_layer(window), (Layer *)action_menu_layer); layer_add_child(window_get_root_layer(window), (Layer *)crumbs_layer); // Click config window_set_click_config_provider_with_context(window, prv_click_config_provider, data); // Init the view model data->view_model = (ActionMenuViewModel) { .cur_level = data->config.root_level, .menu_insets = prv_action_menu_insets(window), .num_dots = 1, }; prv_view_model_did_change(data); } static void prv_action_window_unload(Window *window) { ActionMenuData *data = window_get_user_data(window); // call did close callback so user can cleanup prv_invoke_did_close((ActionMenu *)window); // cleanup animation_unschedule(data->level_change_anim); action_menu_layer_deinit(&data->action_menu_layer); crumbs_layer_deinit(&data->crumbs_layer); applib_free(data); } static void prv_dummy_click_config(void *data) { } ActionMenuLevel *action_menu_get_root_level(ActionMenu *action_menu) { if (!action_menu) return NULL; ActionMenuData *data = window_get_user_data(&action_menu->window); return (ActionMenuLevel *)data->config.root_level; } void *action_menu_get_context(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); return data->config.context; } void action_menu_freeze(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); window_set_click_config_provider(&action_menu->window, prv_dummy_click_config); data->frozen = true; } void action_menu_unfreeze(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); window_set_click_config_provider_with_context(&action_menu->window, prv_click_config_provider, data); data->frozen = false; } bool action_menu_is_frozen(ActionMenu *action_menu) { ActionMenuData *data = window_get_user_data(&action_menu->window); return data->frozen; } void action_menu_close(ActionMenu *action_menu, bool animated) { prv_action_window_pop(animated, action_menu); } void action_menu_set_result_window(ActionMenu *action_menu, Window *result_window) { if (!action_menu) return; // remove existing result window ActionMenuData *data = window_get_user_data(&action_menu->window); if (data->result_window) { prv_remove_window(data->result_window); } // insert new result window if (result_window) { prv_action_window_insert_below(action_menu, result_window); } data->result_window = result_window; } void action_menu_set_align(ActionMenuConfig *config, ActionMenuAlign align) { if (!config) { return; } config->align = align; } ActionMenu *action_menu_open(WindowStack *window_stack, ActionMenuConfig *config) { ActionMenuData *data = applib_type_zalloc(ActionMenuData); data->config = *config; #if SCREEN_COLOR_DEPTH_BITS == 8 // Apply defaults if client didn't assign foreground/background colors if (gcolor_is_invisible(data->config.colors.background)) { data->config.colors.background = ACTION_MENU_DEFAULT_BACKGROUND_COLOR; } if (gcolor_is_invisible((data->config.colors.foreground))) { data->config.colors.foreground = gcolor_legible_over(data->config.colors.background); } #else data->config.colors.background = GColorLightGray; data->config.colors.foreground = GColorBlack; #endif Window *window = &data->action_menu.window; window_init(window, WINDOW_NAME("Action Menu")); window_set_user_data(window, data); window_set_fullscreen(window, true); window_set_background_color(window, GColorBlack); window_set_window_handlers(window, &(WindowHandlers) { .load = prv_action_window_load, .unload = prv_action_window_unload, }); prv_action_window_push(window_stack, &data->action_menu, true /* animated */); return &data->action_menu; } ActionMenu *app_action_menu_open(ActionMenuConfig *config) { return action_menu_open(app_state_get_window_stack(), config); }