mirror of
https://github.com/google/pebble.git
synced 2025-11-19 22:11:02 -05:00
360 lines
13 KiB
C
360 lines
13 KiB
C
/*
|
|
* 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);
|
|
}
|