mirror of
https://github.com/google/pebble.git
synced 2025-07-04 13:57:04 -04:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
252
src/fw/applib/ui/dialogs/actionable_dialog.c
Normal file
252
src/fw/applib/ui/dialogs/actionable_dialog.c
Normal file
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* 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 "actionable_dialog.h"
|
||||
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/ui/bitmap_layer.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/dialogs/dialog_private.h"
|
||||
#include "applib/ui/kino/kino_reel/scale_segmented.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
static void prv_actionable_dialog_load(Window *window) {
|
||||
ActionableDialog *actionable_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = &actionable_dialog->dialog;
|
||||
Layer *window_root_layer = window_get_root_layer(window);
|
||||
|
||||
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
|
||||
KinoReel *icon = dialog_create_icon(dialog);
|
||||
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
|
||||
|
||||
const GRect *bounds = &window_root_layer->bounds;
|
||||
const uint16_t icon_single_line_text_offset_px = 13;
|
||||
const uint16_t left_margin_px = PBL_IF_RECT_ELSE(5, 0);
|
||||
const uint16_t content_and_action_bar_horizontal_spacing = PBL_IF_RECT_ELSE(5, 7);
|
||||
const uint16_t right_margin_px = ACTION_BAR_WIDTH +
|
||||
content_and_action_bar_horizontal_spacing;
|
||||
const uint16_t text_single_line_text_offset_px = icon_single_line_text_offset_px - 1;
|
||||
const GFont dialog_text_font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
|
||||
const int single_line_text_height_px = fonts_get_font_height(dialog_text_font);
|
||||
const int max_text_line_height_px = 2 * single_line_text_height_px + 8;
|
||||
|
||||
const uint16_t status_layer_offset = dialog->show_status_layer ? 6 : 0;
|
||||
uint16_t text_top_margin_px = icon ? icon_size.h + 22 : 6;
|
||||
uint16_t icon_top_margin_px = 18;
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
uint16_t w = PBL_IF_RECT_ELSE(bounds->size.w - ACTION_BAR_WIDTH, bounds->size.w);
|
||||
uint16_t h = STATUS_BAR_LAYER_HEIGHT;
|
||||
|
||||
if (dialog->show_status_layer) {
|
||||
dialog_add_status_bar_layer(dialog, &GRect(x, y, w, h));
|
||||
}
|
||||
|
||||
x = left_margin_px;
|
||||
w = bounds->size.w - left_margin_px - right_margin_px;
|
||||
|
||||
GTextAttributes *text_attributes = NULL;
|
||||
#if PBL_ROUND
|
||||
// Create a GTextAttributes for the TextLayer. Note that the matching
|
||||
// graphics_text_attributes_destroy() will not need to be called here, as the ownership
|
||||
// of text_attributes will be transferred to the TextLayer we assign it to.
|
||||
text_attributes = graphics_text_attributes_create();
|
||||
graphics_text_attributes_enable_screen_text_flow(text_attributes, 8);
|
||||
#endif
|
||||
// Check if the text takes up more than one line. If the dialog has a single line of text,
|
||||
// the icon and line of text are positioned lower so as to be more vertically centered.
|
||||
GContext *ctx = graphics_context_get_current_context();
|
||||
const GTextAlignment text_alignment = PBL_IF_RECT_ELSE(GTextAlignmentCenter, GTextAlignmentRight);
|
||||
{
|
||||
// do all this in a block so we enforce that nobody uses these variables outside of the block
|
||||
// when dealing with round displays, sizes change depending on location.
|
||||
const GRect probe_rect = GRect(x, y + text_single_line_text_offset_px,
|
||||
w, max_text_line_height_px);
|
||||
const uint16_t text_height = graphics_text_layout_get_max_used_size(ctx,
|
||||
dialog->buffer,
|
||||
dialog_text_font,
|
||||
probe_rect,
|
||||
GTextOverflowModeWordWrap,
|
||||
text_alignment,
|
||||
text_attributes).h;
|
||||
if (text_height <= single_line_text_height_px) {
|
||||
text_top_margin_px += text_single_line_text_offset_px;
|
||||
icon_top_margin_px += icon_single_line_text_offset_px;
|
||||
} else {
|
||||
text_top_margin_px += status_layer_offset;
|
||||
icon_top_margin_px += status_layer_offset;
|
||||
}
|
||||
}
|
||||
|
||||
y = text_top_margin_px;
|
||||
h = bounds->size.h - y;
|
||||
|
||||
// Set up the text.
|
||||
TextLayer *text_layer = &dialog->text_layer;
|
||||
text_layer_init_with_parameters(text_layer, &GRect(x, y, w, h),
|
||||
dialog->buffer, dialog_text_font,
|
||||
dialog->text_color, GColorClear, text_alignment,
|
||||
GTextOverflowModeWordWrap);
|
||||
if (text_attributes) {
|
||||
text_layer->should_cache_layout = true;
|
||||
text_layer->layout_cache = text_attributes;
|
||||
}
|
||||
|
||||
layer_add_child(&window->layer, &text_layer->layer);
|
||||
|
||||
// Action bar. If the user hasn't given a custom action bar, we'll create one of the preset
|
||||
// types.
|
||||
if (actionable_dialog->action_bar_type != DialogActionBarCustom) {
|
||||
actionable_dialog->action_bar = action_bar_layer_create();
|
||||
action_bar_layer_set_click_config_provider(actionable_dialog->action_bar,
|
||||
actionable_dialog->config_provider);
|
||||
}
|
||||
ActionBarLayer *action_bar = actionable_dialog->action_bar;
|
||||
if (actionable_dialog->action_bar_type == DialogActionBarConfirm) {
|
||||
#if !defined(RECOVERY_FW)
|
||||
actionable_dialog->select_icon = gbitmap_create_with_resource(
|
||||
RESOURCE_ID_ACTION_BAR_ICON_CHECK);
|
||||
#endif
|
||||
action_bar_layer_set_context(action_bar, window);
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, actionable_dialog->select_icon);
|
||||
} else if (actionable_dialog->action_bar_type == DialogActionBarDecline) {
|
||||
#if !defined(RECOVERY_FW)
|
||||
actionable_dialog->select_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
|
||||
#endif
|
||||
action_bar_layer_set_context(action_bar, window);
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_SELECT, actionable_dialog->select_icon);
|
||||
} else if (actionable_dialog->action_bar_type == DialogActionBarConfirmDecline) {
|
||||
#if !defined(RECOVERY_FW)
|
||||
actionable_dialog->up_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_CHECK);
|
||||
actionable_dialog->down_icon = gbitmap_create_with_resource(RESOURCE_ID_ACTION_BAR_ICON_X);
|
||||
#endif
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, actionable_dialog->up_icon);
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, actionable_dialog->down_icon);
|
||||
action_bar_layer_set_context(action_bar, window);
|
||||
}
|
||||
action_bar_layer_add_to_window(action_bar, window);
|
||||
|
||||
// Icon
|
||||
// On rectangular displays we just center it horizontally b/w the left edge of the display and
|
||||
// the left edge of the action bar
|
||||
#if PBL_RECT
|
||||
x = (grect_get_max_x(bounds) - ACTION_BAR_WIDTH - icon_size.w) / 2;
|
||||
#else
|
||||
// On round displays we right align it with respect to the same imaginary vertical line that the
|
||||
// text is right aligned to
|
||||
x = grect_get_max_x(bounds) - ACTION_BAR_WIDTH - content_and_action_bar_horizontal_spacing -
|
||||
icon_size.w;
|
||||
#endif
|
||||
|
||||
y = icon_top_margin_px;
|
||||
|
||||
if (dialog_init_icon_layer(dialog, icon, GPoint(x, y), true)) {
|
||||
layer_add_child(window_root_layer, &dialog->icon_layer.layer);
|
||||
}
|
||||
|
||||
dialog_load(&actionable_dialog->dialog);
|
||||
}
|
||||
|
||||
static void prv_actionable_dialog_appear(Window *window) {
|
||||
ActionableDialog *actionable_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = actionable_dialog_get_dialog(actionable_dialog);
|
||||
dialog_appear(dialog);
|
||||
}
|
||||
|
||||
static void prv_actionable_dialog_unload(Window *window) {
|
||||
ActionableDialog *actionable_dialog = window_get_user_data(window);
|
||||
dialog_unload(&actionable_dialog->dialog);
|
||||
|
||||
// Destroy action bar if it was a predefined type. If the action bar is custom, it is the user's
|
||||
// responsibility to free it.
|
||||
if (actionable_dialog->action_bar_type != DialogActionBarCustom) {
|
||||
action_bar_layer_destroy(actionable_dialog->action_bar);
|
||||
if (actionable_dialog->action_bar_type == DialogActionBarConfirmDecline) {
|
||||
gbitmap_destroy(actionable_dialog->up_icon);
|
||||
gbitmap_destroy(actionable_dialog->down_icon);
|
||||
} else { // DialogActionBarConfirm || DialogActionBarDecline
|
||||
gbitmap_destroy(actionable_dialog->select_icon);
|
||||
}
|
||||
}
|
||||
if (actionable_dialog->dialog.destroy_on_pop) {
|
||||
applib_free(actionable_dialog);
|
||||
}
|
||||
}
|
||||
|
||||
Dialog *actionable_dialog_get_dialog(ActionableDialog *actionable_dialog) {
|
||||
return &actionable_dialog->dialog;
|
||||
}
|
||||
|
||||
void actionable_dialog_push(ActionableDialog *actionable_dialog, WindowStack *window_stack) {
|
||||
dialog_push(&actionable_dialog->dialog, window_stack);
|
||||
}
|
||||
|
||||
void app_actionable_dialog_push(ActionableDialog *actionable_dialog) {
|
||||
app_dialog_push(&actionable_dialog->dialog);
|
||||
}
|
||||
|
||||
void actionable_dialog_pop(ActionableDialog *actionable_dialog) {
|
||||
dialog_pop(&actionable_dialog->dialog);
|
||||
}
|
||||
|
||||
void actionable_dialog_init(ActionableDialog *actionable_dialog, const char *dialog_name) {
|
||||
PBL_ASSERTN(actionable_dialog);
|
||||
*actionable_dialog = (ActionableDialog){};
|
||||
|
||||
dialog_init(&actionable_dialog->dialog, dialog_name);
|
||||
Window *window = &actionable_dialog->dialog.window;
|
||||
window_set_window_handlers(window, &(WindowHandlers) {
|
||||
.load = prv_actionable_dialog_load,
|
||||
.unload = prv_actionable_dialog_unload,
|
||||
.appear = prv_actionable_dialog_appear,
|
||||
});
|
||||
window_set_user_data(window, actionable_dialog);
|
||||
}
|
||||
|
||||
ActionableDialog *actionable_dialog_create(const char *dialog_name) {
|
||||
// Note: Not exported so no need for padding.
|
||||
ActionableDialog *actionable_dialog = applib_malloc(sizeof(ActionableDialog));
|
||||
if (actionable_dialog) {
|
||||
actionable_dialog_init(actionable_dialog, dialog_name);
|
||||
}
|
||||
return actionable_dialog;
|
||||
}
|
||||
|
||||
void actionable_dialog_set_action_bar_type(ActionableDialog *actionable_dialog,
|
||||
DialogActionBarType action_bar_type,
|
||||
ActionBarLayer *action_bar) {
|
||||
if (action_bar_type == DialogActionBarCustom) {
|
||||
PBL_ASSERTN(action_bar); // Action bar must not be NULL if it is a custom type.
|
||||
actionable_dialog->action_bar = action_bar;
|
||||
} else {
|
||||
actionable_dialog->action_bar = NULL;
|
||||
}
|
||||
actionable_dialog->action_bar_type = action_bar_type;
|
||||
}
|
||||
|
||||
void actionable_dialog_set_click_config_provider(ActionableDialog *actionable_dialog,
|
||||
ClickConfigProvider click_config_provider) {
|
||||
actionable_dialog->config_provider = click_config_provider;
|
||||
}
|
75
src/fw/applib/ui/dialogs/actionable_dialog.h
Normal file
75
src/fw/applib/ui/dialogs/actionable_dialog.h
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "actionable_dialog_private.h"
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/action_bar_layer.h"
|
||||
#include "applib/ui/click.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
|
||||
//! Creates a new ActionableDialog on the heap.
|
||||
//! @param dialog_name The debug name to give the \ref ActionableDialog
|
||||
//! @return Pointer to a \ref ActionableDialog
|
||||
ActionableDialog *actionable_dialog_create(const char *dialog_name);
|
||||
|
||||
//! @internal
|
||||
//! Initializes the passed \ref ActionableDialog
|
||||
//! @param actionable_dialog Pointer to a \ref ActionableDialog to initialize
|
||||
//! @param dialog_name The debug name to give the \ref ActionableDialog
|
||||
void actionable_dialog_init(ActionableDialog *actionable_dialog, const char *dialog_name);
|
||||
|
||||
//! Retrieves the internal Dialog object from the ActionableDialog.
|
||||
//! @param actionable_dialog Pointer to a \ref ActionableDialog from which to grab it's dialog
|
||||
//! @return The underlying \ref Dialog of the given \ref ActionableDialog
|
||||
Dialog *actionable_dialog_get_dialog(ActionableDialog *actionable_dialog);
|
||||
|
||||
//! Sets the type of action bar to used to one of the pre-defined types or a custom one.
|
||||
//! @param actionable_dialog Pointer to a \ref ActioanbleDialog whom which to set
|
||||
//! @param action_bar_type The type of action bar to give the passed dialog
|
||||
//! @param action_bar Pointer to an \ref ActionBarLayer to assign to the dialog
|
||||
//! @note: The pointer to an \ref ActionBarLayer is optional and only required when the
|
||||
//! the \ref DialogActionBarType is \ref DialogActionBarCustom. If the type is not
|
||||
//! custom, then the given action bar will not be set on the dialog, regardless of if
|
||||
//! it is `NULL` or not.
|
||||
void actionable_dialog_set_action_bar_type(ActionableDialog *actionable_dialog,
|
||||
DialogActionBarType action_bar_type,
|
||||
ActionBarLayer *action_bar);
|
||||
|
||||
//! Sets the ClickConfigProvider of the action bar. If the dialog has a custom action bar then
|
||||
//! this function has no effect. The action bar is responsible for setting up it's own click
|
||||
//! config provider
|
||||
//! @param actionable_dialog Pointer to a \ref ActionableDialog to which to set the provider on
|
||||
//! @param click_config_provider The \ref ClickConfigProvider to set
|
||||
void actionable_dialog_set_click_config_provider(ActionableDialog *actionable_dialog,
|
||||
ClickConfigProvider click_config_provider);
|
||||
|
||||
//! @internal
|
||||
//! Pushes the \ref ActionableDialog onto the given window stack.
|
||||
//! @param actionable_dialog Pointer to a \ref ActionableDialog to push
|
||||
//! @param window_stack Pointer to a \ref WindowStack to push the \ref ActionableDialog to
|
||||
void actionable_dialog_push(ActionableDialog *actionable_dialog, WindowStack *window_stack);
|
||||
|
||||
//! Wrapper to call \ref actionable_dialog_push() for an app.
|
||||
//! @note: Put a better comment here when we export
|
||||
void app_actionable_dialog_push(ActionableDialog *actionable_dialog);
|
||||
|
||||
//! Pops the given \ref ActionableDialog from the window stack it was pushed to.
|
||||
//! @param actionable_dialog Pointer to a \ref ActionableDialog to pop
|
||||
void actionable_dialog_pop(ActionableDialog *actionable_dialog);
|
57
src/fw/applib/ui/dialogs/actionable_dialog_private.h
Normal file
57
src/fw/applib/ui/dialogs/actionable_dialog_private.h
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
//! An ActionableDialog is a dialog that has an action bar on the right hand side
|
||||
//! of the window. The user can specify there own custom \ref ActionBarLayer to
|
||||
//! override the default behaviour or specify a \ref ClickConfigProvider to tie
|
||||
//! into the default \ref ActionBarLayer provided by the dialog.
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/graphics/perimeter.h"
|
||||
#include "applib/ui/action_bar_layer.h"
|
||||
#include "applib/ui/click.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
|
||||
//! Different types of action bar. Two commonly used types are built in:
|
||||
//! Confirm and Decline. Alternatively, the user can supply their own
|
||||
//! custom action bar.
|
||||
typedef enum DialogActionBarType {
|
||||
//! SELECT: Confirm icon
|
||||
DialogActionBarConfirm,
|
||||
//! SELECT: Decline icon
|
||||
DialogActionBarDecline,
|
||||
//! UP: Confirm icon, DOWN: Decline icon
|
||||
DialogActionBarConfirmDecline,
|
||||
//! Provide your own action bar
|
||||
DialogActionBarCustom
|
||||
} DialogActionBarType;
|
||||
|
||||
typedef struct ActionableDialog {
|
||||
Dialog dialog;
|
||||
DialogActionBarType action_bar_type;
|
||||
union {
|
||||
struct {
|
||||
GBitmap *select_icon;
|
||||
};
|
||||
struct {
|
||||
GBitmap *up_icon;
|
||||
GBitmap *down_icon;
|
||||
};
|
||||
};
|
||||
ActionBarLayer *action_bar;
|
||||
ClickConfigProvider config_provider;
|
||||
} ActionableDialog;
|
117
src/fw/applib/ui/dialogs/bt_conn_dialog.c
Normal file
117
src/fw/applib/ui/dialogs/bt_conn_dialog.c
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 "bt_conn_dialog.h"
|
||||
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/dialogs/dialog_private.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pebble_tasks.h"
|
||||
#include "kernel/ui/modals/modal_manager.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "syscall/syscall.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
static void prv_handle_comm_session_event(PebbleEvent *e, void *context) {
|
||||
BtConnDialog *bt_dialog = context;
|
||||
if (!e->bluetooth.comm_session_event.is_system) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e->bluetooth.comm_session_event.is_open) {
|
||||
if (bt_dialog->connected_handler) {
|
||||
bt_dialog->connected_handler(true, bt_dialog->context);
|
||||
}
|
||||
// handler to NULL so it won't be called again during the unload
|
||||
bt_dialog->connected_handler = NULL;
|
||||
dialog_pop(&bt_dialog->dialog.dialog);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_bt_dialog_unload(void *context) {
|
||||
BtConnDialog *bt_dialog = context;
|
||||
event_service_client_unsubscribe(&bt_dialog->pebble_app_event_sub);
|
||||
if (bt_dialog->connected_handler) {
|
||||
bt_dialog->connected_handler(false, bt_dialog->context);
|
||||
}
|
||||
if (bt_dialog->owns_buffer) {
|
||||
applib_free(bt_dialog->text_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void bt_conn_dialog_push(BtConnDialog *bt_dialog, BtConnDialogResultHandler handler,
|
||||
void *context) {
|
||||
if (!bt_dialog) {
|
||||
bt_dialog = bt_conn_dialog_create();
|
||||
if (!bt_dialog) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
bt_dialog->connected_handler = handler;
|
||||
bt_dialog->context = context;
|
||||
|
||||
bt_dialog->pebble_app_event_sub = (EventServiceInfo) {
|
||||
.type = PEBBLE_COMM_SESSION_EVENT,
|
||||
.handler = prv_handle_comm_session_event,
|
||||
.context = bt_dialog
|
||||
};
|
||||
event_service_client_subscribe(&bt_dialog->pebble_app_event_sub);
|
||||
|
||||
WindowStack *window_stack = NULL;
|
||||
if (pebble_task_get_current() == PebbleTask_App) {
|
||||
window_stack = app_state_get_window_stack();
|
||||
} else {
|
||||
// Bluetooth disconnection events are always displayed at maximum priority.
|
||||
window_stack = modal_manager_get_window_stack(ModalPriorityCritical);
|
||||
}
|
||||
simple_dialog_push(&bt_dialog->dialog, window_stack);
|
||||
}
|
||||
|
||||
BtConnDialog *bt_conn_dialog_create(void) {
|
||||
BtConnDialog *bt_dialog = applib_malloc(sizeof(BtConnDialog));
|
||||
bt_conn_dialog_init(bt_dialog, NULL, 0);
|
||||
return bt_dialog;
|
||||
}
|
||||
|
||||
void bt_conn_dialog_init(BtConnDialog *bt_dialog, char *text_buffer, size_t buffer_size) {
|
||||
memset(bt_dialog, 0, sizeof(BtConnDialog));
|
||||
|
||||
simple_dialog_init(&bt_dialog->dialog, "Bluetooth Disconnected");
|
||||
Dialog *dialog = &bt_dialog->dialog.dialog;
|
||||
|
||||
size_t len = sys_i18n_get_length("Check bluetooth connection");
|
||||
if (text_buffer) {
|
||||
PBL_ASSERTN(len < buffer_size);
|
||||
bt_dialog->text_buffer = text_buffer;
|
||||
bt_dialog->owns_buffer = false;
|
||||
} else {
|
||||
buffer_size = len + 1;
|
||||
bt_dialog->text_buffer = applib_malloc(buffer_size);
|
||||
bt_dialog->owns_buffer = true;
|
||||
}
|
||||
|
||||
sys_i18n_get_with_buffer("Check bluetooth connection", bt_dialog->text_buffer, buffer_size);
|
||||
dialog_set_text(dialog, bt_dialog->text_buffer);
|
||||
dialog_set_icon(dialog, RESOURCE_ID_WATCH_DISCONNECTED_LARGE);
|
||||
dialog_show_status_bar_layer(dialog, true);
|
||||
dialog_set_callbacks(dialog, &(DialogCallbacks) {
|
||||
.unload = prv_bt_dialog_unload
|
||||
}, bt_dialog);
|
||||
}
|
52
src/fw/applib/ui/dialogs/bt_conn_dialog.h
Normal file
52
src/fw/applib/ui/dialogs/bt_conn_dialog.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/event_service_client.h"
|
||||
#include "applib/ui/dialogs/simple_dialog.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef void (*BtConnDialogResultHandler)(bool connected, void *context);
|
||||
|
||||
typedef struct {
|
||||
SimpleDialog dialog;
|
||||
EventServiceInfo pebble_app_event_sub;
|
||||
BtConnDialogResultHandler connected_handler;
|
||||
void *context;
|
||||
char *text_buffer;
|
||||
bool owns_buffer;
|
||||
} BtConnDialog;
|
||||
|
||||
//! @internal
|
||||
//! Wrapper around a \ref SimpleDialog for showing a bluetooth connection event.
|
||||
//! @param bt_dialog Pointer to the \ref BtConnDialog to push
|
||||
//! @param handler The \ref BtConnDialogResultHandler to be called when
|
||||
//! bluetooth is reconnected.
|
||||
//! @param context The context to pass to the handler
|
||||
void bt_conn_dialog_push(BtConnDialog *bt_dialog, BtConnDialogResultHandler handler, void *context);
|
||||
|
||||
//! @internal
|
||||
//! Allocates a \ref BtConnDialog on the heap and returns it
|
||||
//! @return Pointer to a \ref BtConnDialog
|
||||
BtConnDialog *bt_conn_dialog_create(void);
|
||||
|
||||
//! @internal
|
||||
//! Initializes a \ref BtConnDialog
|
||||
//! @param bt_dialog Pointer to the \ref BtConnDialog to initialize
|
||||
void bt_conn_dialog_init(BtConnDialog *bt_dialog, char *text_buffer, size_t buffer_size);
|
112
src/fw/applib/ui/dialogs/confirmation_dialog.c
Normal file
112
src/fw/applib/ui/dialogs/confirmation_dialog.c
Normal file
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 "confirmation_dialog.h"
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/action_bar_layer.h"
|
||||
#include "applib/ui/dialogs/actionable_dialog.h"
|
||||
#include "applib/ui/dialogs/dialog_private.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
struct ConfirmationDialog {
|
||||
ActionableDialog action_dialog;
|
||||
ActionBarLayer action_bar;
|
||||
GBitmap confirm_icon;
|
||||
GBitmap decline_icon;
|
||||
};
|
||||
|
||||
ConfirmationDialog *confirmation_dialog_create(const char *dialog_name) {
|
||||
// Note: This isn't a memory leak as the ConfirmationDialog has the action dialog as its
|
||||
// first member, so when we call init and pass the ConfirmationDialog as the argument, when
|
||||
// it frees the associated data, it actually frees the ConfirmationDialog.
|
||||
ConfirmationDialog *confirmation_dialog = task_zalloc_check(sizeof(ConfirmationDialog));
|
||||
if (!gbitmap_init_with_resource(&confirmation_dialog->confirm_icon,
|
||||
RESOURCE_ID_ACTION_BAR_ICON_CHECK)) {
|
||||
task_free(confirmation_dialog);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!gbitmap_init_with_resource(&confirmation_dialog->decline_icon,
|
||||
RESOURCE_ID_ACTION_BAR_ICON_X)) {
|
||||
gbitmap_deinit(&confirmation_dialog->confirm_icon);
|
||||
task_free(confirmation_dialog);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// In order to create a custom ActionDialog type, we need to create an action bar
|
||||
ActionBarLayer *action_bar = &confirmation_dialog->action_bar;
|
||||
action_bar_layer_init(action_bar);
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_UP, &confirmation_dialog->confirm_icon);
|
||||
action_bar_layer_set_icon(action_bar, BUTTON_ID_DOWN, &confirmation_dialog->decline_icon);
|
||||
action_bar_layer_set_background_color(action_bar, GColorBlack);
|
||||
action_bar_layer_set_context(action_bar, confirmation_dialog);
|
||||
|
||||
// Create the underlying actionable dialog as a custom type
|
||||
ActionableDialog *action_dialog = &confirmation_dialog->action_dialog;
|
||||
actionable_dialog_init(action_dialog, dialog_name);
|
||||
actionable_dialog_set_action_bar_type(action_dialog, DialogActionBarCustom, action_bar);
|
||||
|
||||
return confirmation_dialog;
|
||||
}
|
||||
|
||||
Dialog *confirmation_dialog_get_dialog(ConfirmationDialog *confirmation_dialog) {
|
||||
if (confirmation_dialog == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return actionable_dialog_get_dialog(&confirmation_dialog->action_dialog);
|
||||
}
|
||||
|
||||
ActionBarLayer *confirmation_dialog_get_action_bar(ConfirmationDialog *confirmation_dialog) {
|
||||
if (confirmation_dialog == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
return &confirmation_dialog->action_bar;
|
||||
}
|
||||
|
||||
void confirmation_dialog_set_click_config_provider(ConfirmationDialog *confirmation_dialog,
|
||||
ClickConfigProvider click_config_provider) {
|
||||
if (confirmation_dialog == NULL) {
|
||||
return;
|
||||
}
|
||||
ActionBarLayer *action_bar = &confirmation_dialog->action_bar;
|
||||
action_bar_layer_set_click_config_provider(action_bar, click_config_provider);
|
||||
}
|
||||
|
||||
void confirmation_dialog_push(ConfirmationDialog *confirmation_dialog, WindowStack *window_stack) {
|
||||
actionable_dialog_push(&confirmation_dialog->action_dialog, window_stack);
|
||||
}
|
||||
|
||||
void app_confirmation_dialog_push(ConfirmationDialog *confirmation_dialog) {
|
||||
app_actionable_dialog_push(&confirmation_dialog->action_dialog);
|
||||
}
|
||||
|
||||
void confirmation_dialog_pop(ConfirmationDialog *confirmation_dialog) {
|
||||
if (confirmation_dialog == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
action_bar_layer_remove_from_window(&confirmation_dialog->action_bar);
|
||||
action_bar_layer_deinit(&confirmation_dialog->action_bar);
|
||||
|
||||
gbitmap_deinit(&confirmation_dialog->confirm_icon);
|
||||
gbitmap_deinit(&confirmation_dialog->decline_icon);
|
||||
|
||||
actionable_dialog_pop(&confirmation_dialog->action_dialog);
|
||||
}
|
65
src/fw/applib/ui/dialogs/confirmation_dialog.h
Normal file
65
src/fw/applib/ui/dialogs/confirmation_dialog.h
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
//! A ConfirmationDialog is a wrapper around an ActionableDialog implementing
|
||||
//! the common features provided by a confirmation window. The user specifies
|
||||
//! callbacks for confirm/decline and can also override the back button behaviour.
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/action_bar_layer.h"
|
||||
#include "applib/ui/click.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
|
||||
typedef struct ConfirmationDialog ConfirmationDialog;
|
||||
|
||||
//! Creates a ConfirmationDialog on the heap.
|
||||
//! @param dialog_name The debug name to give the created dialog
|
||||
//! @return Pointer to the created \ref ConfirmationDialog
|
||||
ConfirmationDialog *confirmation_dialog_create(const char *dialog_name);
|
||||
|
||||
//! Retrieves the internal Dialog object from the ConfirmationDialog.
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog whom's dialog to get
|
||||
//! @return Pointer to the underlying \ref Dialog
|
||||
Dialog *confirmation_dialog_get_dialog(ConfirmationDialog *confirmation_dialog);
|
||||
|
||||
//! Retrieves the internal ActionBarLayer object from the ConfirmationDialog.
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog whom's
|
||||
//! \ref ActionBarLayer to get
|
||||
//! @return \ref ActionBarLayer
|
||||
ActionBarLayer *confirmation_dialog_get_action_bar(ConfirmationDialog *confirmation_dialog);
|
||||
|
||||
//! Sets the click ClickConfigProvider for the ConfirmationDialog.
|
||||
//! Passes the ConfirmationDialog as the context to the click handlers.
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to which to set
|
||||
//! @param click_config_provider The \ref ClickConfigProvider to set
|
||||
void confirmation_dialog_set_click_config_provider(ConfirmationDialog *confirmation_dialog,
|
||||
ClickConfigProvider click_config_provider);
|
||||
|
||||
//! Pushes the ConfirmationDialog onto the given window stack
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to push
|
||||
//! @param window_stack Pointer to a \ref WindowStack to push the dialog to
|
||||
void confirmation_dialog_push(ConfirmationDialog *confirmation_dialog, WindowStack *window_stack);
|
||||
|
||||
//! Wrapper for an app to call \ref confirmation_dialog_push()
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to push to
|
||||
//! the app's window stack
|
||||
//! @note: Put a better comment here before exporting
|
||||
void app_confirmation_dialog_push(ConfirmationDialog *confirmation_dialog);
|
||||
|
||||
//! Pops the ConfirmationDialog from the window stack.
|
||||
//! @param confirmation_dialog Pointer to a \ref ConfirmationDialog to pop from its window stack
|
||||
void confirmation_dialog_pop(ConfirmationDialog *confirmation_dialog);
|
105
src/fw/applib/ui/dialogs/dialog.c
Normal file
105
src/fw/applib/ui/dialogs/dialog.c
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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 "dialog.h"
|
||||
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
void dialog_set_fullscreen(Dialog *dialog, bool is_fullscreen) {
|
||||
window_set_fullscreen(&dialog->window, is_fullscreen);
|
||||
}
|
||||
|
||||
void dialog_show_status_bar_layer(Dialog *dialog, bool show_status_layer) {
|
||||
dialog->show_status_layer = show_status_layer;
|
||||
}
|
||||
|
||||
void dialog_set_text(Dialog *dialog, const char *text) {
|
||||
dialog_set_text_buffer(dialog, NULL, false);
|
||||
uint16_t len = strlen(text);
|
||||
dialog->is_buffer_owned = true;
|
||||
dialog->buffer = applib_malloc(len + 1);
|
||||
strncpy(dialog->buffer, text, len + 1);
|
||||
text_layer_set_text(&dialog->text_layer, dialog->buffer);
|
||||
}
|
||||
|
||||
void dialog_set_text_buffer(Dialog *dialog, char *buffer, bool take_ownership) {
|
||||
if (dialog->buffer && dialog->is_buffer_owned) {
|
||||
applib_free(dialog->buffer);
|
||||
}
|
||||
dialog->is_buffer_owned = take_ownership;
|
||||
dialog->buffer = buffer;
|
||||
}
|
||||
|
||||
void dialog_set_text_color(Dialog *dialog, GColor text_color) {
|
||||
dialog->text_color = PBL_IF_COLOR_ELSE(text_color, GColorBlack);
|
||||
text_layer_set_text_color(&dialog->text_layer, dialog->text_color);
|
||||
}
|
||||
|
||||
void dialog_set_background_color(Dialog *dialog, GColor background_color) {
|
||||
window_set_background_color(&dialog->window, PBL_IF_COLOR_ELSE(background_color, GColorWhite));
|
||||
}
|
||||
|
||||
void dialog_set_icon(Dialog *dialog, uint32_t icon_id) {
|
||||
if (dialog->icon_id == icon_id) {
|
||||
// Why bother destroying and then recreating the same icon?
|
||||
// Restart the animation to preserve behavior.
|
||||
kino_layer_rewind(&dialog->icon_layer);
|
||||
kino_layer_play(&dialog->icon_layer);
|
||||
return;
|
||||
}
|
||||
|
||||
dialog->icon_id = icon_id;
|
||||
if (window_is_loaded(&dialog->window)) {
|
||||
kino_layer_set_reel_with_resource(&dialog->icon_layer, icon_id);
|
||||
}
|
||||
}
|
||||
|
||||
void dialog_set_icon_animate_direction(Dialog *dialog, DialogIconAnimationDirection direction) {
|
||||
dialog->icon_anim_direction = direction;
|
||||
}
|
||||
|
||||
void dialog_set_vibe(Dialog *dialog, bool vibe_on_show) {
|
||||
dialog->vibe_on_show = vibe_on_show;
|
||||
}
|
||||
|
||||
void dialog_set_timeout(Dialog *dialog, uint32_t timeout) {
|
||||
dialog->timeout = timeout;
|
||||
}
|
||||
|
||||
void dialog_set_callbacks(Dialog *dialog, const DialogCallbacks *callbacks,
|
||||
void *callback_context) {
|
||||
if (!callbacks) {
|
||||
dialog->callbacks = (DialogCallbacks) {};
|
||||
return;
|
||||
}
|
||||
|
||||
dialog->callbacks = *callbacks;
|
||||
dialog->callback_context = callback_context;
|
||||
}
|
||||
|
||||
void dialog_set_destroy_on_pop(Dialog *dialog, bool destroy_on_pop) {
|
||||
dialog->destroy_on_pop = destroy_on_pop;
|
||||
}
|
||||
|
||||
void dialog_appear(Dialog *dialog) {
|
||||
KinoLayer *icon_layer = &dialog->icon_layer;
|
||||
if (kino_layer_get_reel(icon_layer)) {
|
||||
kino_layer_play(icon_layer);
|
||||
}
|
||||
}
|
145
src/fw/applib/ui/dialogs/dialog.h
Normal file
145
src/fw/applib/ui/dialogs/dialog.h
Normal file
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/app_timer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/status_bar_layer.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DIALOG_MAX_MESSAGE_LEN 140
|
||||
#define DIALOG_IS_ANIMATED true
|
||||
|
||||
// TODO PBL-38106: Replace uses of DIALOG_TIMEOUT_DEFAULT with preferred_result_display_duration()
|
||||
// The number of milliseconds it takes for the dialog to automatically go away if has_timeout is
|
||||
// set to true.
|
||||
#define DIALOG_TIMEOUT_DEFAULT (1000)
|
||||
#define DIALOG_TIMEOUT_INFINITE (0)
|
||||
|
||||
struct Dialog;
|
||||
|
||||
typedef void (*DialogCallback)(void *context);
|
||||
|
||||
typedef enum {
|
||||
// most dialogs will be pushed. FromRight works best for that (it is default)
|
||||
DialogIconAnimateNone = 0,
|
||||
DialogIconAnimationFromRight,
|
||||
DialogIconAnimationFromLeft,
|
||||
} DialogIconAnimationDirection;
|
||||
|
||||
typedef struct {
|
||||
DialogCallback load;
|
||||
DialogCallback unload;
|
||||
} DialogCallbacks;
|
||||
|
||||
//! A newly created Dialog will have the following defaults:
|
||||
//! * Fullscreen: True,
|
||||
//! * Show Status Layer: False,
|
||||
//! * Text Color: GColorBlack,
|
||||
//! * Background Color: GColorLightGray (GColorWhite for tintin)
|
||||
//! * Vibe: False
|
||||
|
||||
// Dialog object used as the core of other dialog types. The Dialog object shouldn't be used
|
||||
// directly to create a dialog window. Instead, one of specific types that wraps a Dialog should
|
||||
// be used, such as the SimpleDialog.
|
||||
typedef struct Dialog {
|
||||
Window window;
|
||||
|
||||
// Time out. The dialog can be configured to timeout after DIALOG_TIMEOUT_DURATION ms.
|
||||
uint32_t timeout;
|
||||
AppTimer *timer;
|
||||
|
||||
// Buffer for the main text of the dialog.
|
||||
char *buffer;
|
||||
bool is_buffer_owned;
|
||||
|
||||
// True if the dialog should vibrate when it opens, false otherwise.
|
||||
bool vibe_on_show;
|
||||
|
||||
bool show_status_layer;
|
||||
StatusBarLayer status_layer;
|
||||
|
||||
// Icon for the dialog.
|
||||
KinoLayer icon_layer;
|
||||
uint32_t icon_id;
|
||||
DialogIconAnimationDirection icon_anim_direction;
|
||||
|
||||
// Text layer on which the main text goes.
|
||||
TextLayer text_layer;
|
||||
|
||||
// Color of the dialog text.
|
||||
GColor text_color;
|
||||
|
||||
// Callbacks and context for unloading the dialog. The user is allowed to set these callbacks to
|
||||
// perform actions (such as freeing resources) when the dialog window has appeared or is unloaded.
|
||||
// They are also useful if the user is wanted to change the KinoReel for the exit animation.
|
||||
DialogCallbacks callbacks;
|
||||
void *callback_context;
|
||||
|
||||
bool destroy_on_pop;
|
||||
} Dialog;
|
||||
|
||||
// If set to true, sets the dialog window to fullscreen.
|
||||
void dialog_set_fullscreen(Dialog *dialog, bool is_fullscreen);
|
||||
|
||||
// If set to true, shows a status bar layer at the top of the dialog.
|
||||
void dialog_show_status_bar_layer(Dialog *dialog, bool show_status_layer);
|
||||
|
||||
// Sets the dialog's main text.
|
||||
// Allocates a buffer on the application heap to store the text. The dialog will retain ownership of
|
||||
// the buffer and will free it if different text is set or a different buffer is specified with
|
||||
// dialog_set_text_buffer.
|
||||
void dialog_set_text(Dialog *dialog, const char *text);
|
||||
|
||||
// Sets the dialog's main text using the string in the buffer passed. Any buffer owned by the dialog
|
||||
// will be freed when the dialog is unloaded or when another buffer or text (dialog_set_text) is
|
||||
// supplied
|
||||
void dialog_set_text_buffer(Dialog *dialog, char *buffer, bool take_ownership);
|
||||
|
||||
// Sets the color of the dialog's text.
|
||||
// if SCREEN_COLOR_DEPTH_BITS == 1 then the color will always be set to black
|
||||
void dialog_set_text_color(Dialog *dialog, GColor text_color);
|
||||
|
||||
// Sets the background color of the dialog window.
|
||||
// if SCREEN_COLOR_DEPTH_BITS == 1 then the color will always be set to white
|
||||
void dialog_set_background_color(Dialog *dialog, GColor background_color);
|
||||
|
||||
// Sets the icon displayed by the dialog.
|
||||
void dialog_set_icon(Dialog *dialog, uint32_t icon_id);
|
||||
|
||||
// Sets the direction from which in the icon animates in.
|
||||
void dialog_set_icon_animate_direction(Dialog *dialog, DialogIconAnimationDirection direction);
|
||||
|
||||
// If set to true, the dialog will emit a short vibe pulse when first opened.
|
||||
void dialog_set_vibe(Dialog *dialog, bool vibe_on_show);
|
||||
|
||||
// Set the timeout of the dialog. Using DIALOG_TIMEOUT_DEFAULT will set the timeout to 1s, using
|
||||
// DIALOG_TIMEOUT_INFINITE (0) will disable the timeout
|
||||
void dialog_set_timeout(Dialog *dialog, uint32_t timeout);
|
||||
|
||||
// Allows the user to provide a custom callback and optionally a custom context for unloading the
|
||||
// dialog. This callback will be called from the dialog's own unload function and can be used
|
||||
// to clean up resources used by the dialog such as icons. If the unload context is NULL, the
|
||||
// parent dialog object will be passed instead.
|
||||
void dialog_set_callbacks(Dialog *dialog, const DialogCallbacks *callbacks,
|
||||
void *callback_context);
|
||||
|
||||
// Enable or disable automatically destroying the dialog when it's popped.
|
||||
void dialog_set_destroy_on_pop(Dialog *dialog, bool destroy_on_pop);
|
176
src/fw/applib/ui/dialogs/dialog_private.c
Normal file
176
src/fw/applib/ui/dialogs/dialog_private.c
Normal file
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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 "dialog_private.h"
|
||||
|
||||
#include "applib/app_timer.h"
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/kino/kino_reel/transform.h"
|
||||
#include "applib/ui/kino/kino_reel/scale_segmented.h"
|
||||
#include "applib/ui/kino/kino_reel_pdci.h"
|
||||
#include "applib/ui/vibes.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
static void prv_app_timer_callback(void *context) {
|
||||
dialog_pop(context);
|
||||
}
|
||||
|
||||
void dialog_init(Dialog *dialog, const char *dialog_name) {
|
||||
PBL_ASSERTN(dialog);
|
||||
*dialog = (Dialog){};
|
||||
|
||||
window_init(&dialog->window, dialog_name);
|
||||
window_set_background_color(&dialog->window, PBL_IF_COLOR_ELSE(GColorLightGray, GColorWhite));
|
||||
|
||||
// initial values
|
||||
dialog->icon_anim_direction = DialogIconAnimationFromRight;
|
||||
dialog->destroy_on_pop = true;
|
||||
dialog->text_color = GColorBlack;
|
||||
}
|
||||
|
||||
void dialog_pop(Dialog *dialog) {
|
||||
window_stack_remove(&dialog->window, DIALOG_IS_ANIMATED);
|
||||
}
|
||||
|
||||
void dialog_push(Dialog *dialog, WindowStack *window_stack) {
|
||||
window_stack_push(window_stack, &dialog->window, DIALOG_IS_ANIMATED);
|
||||
}
|
||||
|
||||
void app_dialog_push(Dialog *dialog) {
|
||||
dialog_push(dialog, app_state_get_window_stack());
|
||||
}
|
||||
|
||||
// Loads the core dialog. Should be called from each dialog window's load callback.
|
||||
void dialog_load(Dialog *dialog) {
|
||||
if (dialog->vibe_on_show) {
|
||||
vibes_short_pulse();
|
||||
}
|
||||
|
||||
if (dialog->timeout != DIALOG_TIMEOUT_INFINITE) {
|
||||
dialog->timer = app_timer_register(dialog->timeout, prv_app_timer_callback, dialog);
|
||||
}
|
||||
|
||||
// Calls the user-given load callback, if it exists. If the user gave a non-null context,
|
||||
// the function will use that, otherwise it will default to use the default context of the
|
||||
// containing dialog.
|
||||
if (dialog->callbacks.load) {
|
||||
if (dialog->callback_context) {
|
||||
dialog->callbacks.load(dialog->callback_context);
|
||||
} else {
|
||||
dialog->callbacks.load(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unloads the core dialog. Should be called from each dialog window's unload callback.
|
||||
void dialog_unload(Dialog *dialog) {
|
||||
app_timer_cancel(dialog->timer);
|
||||
|
||||
if (dialog->show_status_layer) {
|
||||
status_bar_layer_deinit(&dialog->status_layer);
|
||||
}
|
||||
|
||||
dialog_set_icon(dialog, INVALID_RESOURCE);
|
||||
text_layer_deinit(&dialog->text_layer);
|
||||
kino_layer_deinit(&dialog->icon_layer);
|
||||
|
||||
if (dialog->buffer && dialog->is_buffer_owned) {
|
||||
applib_free(dialog->buffer);
|
||||
}
|
||||
|
||||
// Calls the user-given unload callback, if it exists. If the user gave a non-null context,
|
||||
// the function will use that, otherwise it will default to use the default context of the
|
||||
// containing dialog.
|
||||
if (dialog->callbacks.unload) {
|
||||
if (dialog->callback_context) {
|
||||
dialog->callbacks.unload(dialog->callback_context);
|
||||
} else {
|
||||
dialog->callbacks.unload(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KinoReel *dialog_create_icon(Dialog *dialog) {
|
||||
return kino_reel_create_with_resource_system(SYSTEM_APP, dialog->icon_id);
|
||||
}
|
||||
|
||||
bool dialog_init_icon_layer(Dialog *dialog, KinoReel *image,
|
||||
GPoint icon_origin, bool animated) {
|
||||
if (!image) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const GRect icon_rect = (GRect) {
|
||||
.origin = icon_origin,
|
||||
.size = kino_reel_get_size(image)
|
||||
};
|
||||
|
||||
KinoLayer *icon_layer = &dialog->icon_layer;
|
||||
kino_layer_init(icon_layer, &icon_rect);
|
||||
layer_set_clips(&icon_layer->layer, false);
|
||||
|
||||
GRect from = icon_rect;
|
||||
// Animate from off screen. We need to be at least -80, since that is our largest icon size.
|
||||
const int16_t DISP_OFFSET = 80;
|
||||
if (dialog->icon_anim_direction == DialogIconAnimationFromLeft) {
|
||||
from.origin.x = -DISP_OFFSET;
|
||||
} else if (dialog->icon_anim_direction == DialogIconAnimationFromRight) {
|
||||
from.origin.x = DISP_OFFSET;
|
||||
}
|
||||
|
||||
const int16_t ICON_TARGET_PT_X = icon_rect.size.w;
|
||||
const int16_t ICON_TARGET_PT_Y = (icon_rect.size.h / 2);
|
||||
|
||||
KinoReel *reel = NULL;
|
||||
if (animated) {
|
||||
reel = kino_reel_scale_segmented_create(image, true, icon_rect);
|
||||
kino_reel_transform_set_from_frame(reel, from);
|
||||
kino_reel_transform_set_transform_duration(reel, 300);
|
||||
kino_reel_scale_segmented_set_deflate_effect(reel, 10);
|
||||
kino_reel_scale_segmented_set_delay_by_distance(
|
||||
reel, GPoint(ICON_TARGET_PT_X, ICON_TARGET_PT_Y));
|
||||
}
|
||||
|
||||
if (!reel) {
|
||||
// Fall back to using the image reel as is which could be an animation without the scaling
|
||||
reel = image;
|
||||
}
|
||||
|
||||
kino_layer_set_reel(icon_layer, reel, true);
|
||||
kino_layer_play(icon_layer);
|
||||
|
||||
uint32_t icon_duration = kino_reel_get_duration(image);
|
||||
if (dialog->timeout != DIALOG_TIMEOUT_INFINITE && // Don't shorten infinite dialogs
|
||||
icon_duration != PLAY_DURATION_INFINITE && // Don't extend dialogs with infinite animations
|
||||
icon_duration > dialog->timeout) {
|
||||
// The finite image animation is longer, increase the finite dialog timeout
|
||||
dialog_set_timeout(dialog, icon_duration);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void dialog_add_status_bar_layer(Dialog *dialog, const GRect *status_layer_frame) {
|
||||
StatusBarLayer *status_layer = &dialog->status_layer;
|
||||
status_bar_layer_init(status_layer);
|
||||
layer_set_frame(&status_layer->layer, status_layer_frame);
|
||||
status_bar_layer_set_colors(status_layer, GColorClear, dialog->text_color);
|
||||
layer_add_child(&dialog->window.layer, &status_layer->layer);
|
||||
}
|
72
src/fw/applib/ui/dialogs/dialog_private.h
Normal file
72
src/fw/applib/ui/dialogs/dialog_private.h
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
|
||||
//! Initializes the dialog.
|
||||
//! @param dialog Pointer to a \ref Dialog to initialize
|
||||
//! @param dialog_name The debug name to give the dialog
|
||||
void dialog_init(Dialog *dialog, const char *dialog_name);
|
||||
|
||||
//! Pushes the dialog onto the window stack.
|
||||
//! @param dialog Pointer to a \ref Dialog to push
|
||||
//! @param window_stack Pointer to a \ref WindowStack to push the \ref Dialog to
|
||||
void dialog_push(Dialog *dialog, WindowStack *window_stack);
|
||||
|
||||
//! Wrapper to call \ref dialog_push() for an application
|
||||
//! @note: Put a better comment here when we export
|
||||
void app_dialog_push(Dialog *dialog);
|
||||
|
||||
//! Pops the dialog off the window stack.
|
||||
//! @param dialog Pointer to a \ref Dialog to push
|
||||
void dialog_pop(Dialog *dialog);
|
||||
|
||||
//! This function is called by each type of dialog's load functions to execute common dialog code.
|
||||
//! @param dialog Pointer to a \ref Dialog to load
|
||||
void dialog_load(Dialog *dialog);
|
||||
|
||||
//! Displays the icon by playing the kino layer
|
||||
//! @param dialog Pointer to the \ref Dialog to appear
|
||||
void dialog_appear(Dialog *dialog);
|
||||
|
||||
//! This function is called by each type of dialog's unload function. The dialog_context is the
|
||||
//! the dialog object being unloaded.
|
||||
//! @param dialog Pointer to a \ref Dialog to unload
|
||||
void dialog_unload(Dialog *dialog);
|
||||
|
||||
//! Draw the status layer on the dialog.
|
||||
//! @param dialog Pointer to a \ref Dialog to draw the status layer on
|
||||
//! @param status_layer_frame The frame of the status layer
|
||||
void dialog_add_status_bar_layer(Dialog *dialog, const GRect *status_layer_frame);
|
||||
|
||||
//! Create the icon for the dialog.
|
||||
//! @param dialog Pointer to a \ref Dialog from which to grab the icon id
|
||||
//! @return the \ref KinoReel for the dialog's icon
|
||||
KinoReel *dialog_create_icon(Dialog *dialog);
|
||||
|
||||
//! Initialize the dialog's icon layer with the provided image and frame origin.
|
||||
//! @param dialog Pointer to \ref Dialog from which to initialize it's \ref KinoLayer
|
||||
//! @param image Pointer to a \ref KinoReel to put on the dialog's \ref KinoLayer
|
||||
//! @param icon_origin The starting point for the icon, if it is being animated,
|
||||
//! this is the point it will animate to.
|
||||
//! @param animated `True` if animated, otherwise `False`
|
||||
//! @return `True` if successfully initialized the dialog's \ref KinoLayer, otherwise
|
||||
//! `False`
|
||||
bool dialog_init_icon_layer(Dialog *dialog, KinoReel *image,
|
||||
GPoint icon_origin, bool animated);
|
443
src/fw/applib/ui/dialogs/expandable_dialog.c
Normal file
443
src/fw/applib/ui/dialogs/expandable_dialog.c
Normal file
|
@ -0,0 +1,443 @@
|
|||
/*
|
||||
* 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 "expandable_dialog.h"
|
||||
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/ui/bitmap_layer.h"
|
||||
#include "applib/ui/dialogs/dialog_private.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "applib/ui/window_private.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "resource/resource.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static void prv_show_action_bar_icon(ExpandableDialog *expandable_dialog, ButtonId button_id) {
|
||||
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
|
||||
const GBitmap *icon = (button_id ==
|
||||
BUTTON_ID_UP) ? expandable_dialog->up_icon : expandable_dialog->down_icon;
|
||||
action_bar_layer_set_icon_animated(action_bar, button_id, icon,
|
||||
expandable_dialog->show_action_icon_animated);
|
||||
|
||||
ActionBarLayerIconPressAnimation animation = (button_id == BUTTON_ID_UP)
|
||||
? ActionBarLayerIconPressAnimationMoveUp
|
||||
: ActionBarLayerIconPressAnimationMoveDown;
|
||||
action_bar_layer_set_icon_press_animation(action_bar, button_id, animation);
|
||||
}
|
||||
|
||||
// Manually scrolls the scroll layer up or down. The manual scrolling is required so that the
|
||||
// click handlers of the scroll layer and the action bar play nicely together.
|
||||
static void prv_manual_scroll(ScrollLayer *scroll_layer, int8_t dir) {
|
||||
scroll_layer_scroll(scroll_layer, dir, true);
|
||||
}
|
||||
|
||||
static void prv_offset_changed_handler(ScrollLayer *scroll_layer, void *context) {
|
||||
ExpandableDialog *expandable_dialog = context;
|
||||
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
|
||||
GPoint offset = scroll_layer_get_content_offset(scroll_layer);
|
||||
|
||||
if (!expandable_dialog->show_action_bar) {
|
||||
// Prematurely return if we are not showing the action bar.
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset.y < 0) {
|
||||
// We have scrolled down, so we want to display the up arrow.
|
||||
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_UP);
|
||||
} else if (offset.y == 0) {
|
||||
// Hide the up arrow as we've reached the top.
|
||||
action_bar_layer_clear_icon(action_bar, BUTTON_ID_UP);
|
||||
}
|
||||
|
||||
Layer *layer = scroll_layer_get_layer(scroll_layer);
|
||||
const GRect *bounds = &layer->bounds;
|
||||
GSize content_size = scroll_layer_get_content_size(scroll_layer);
|
||||
|
||||
if (offset.y + content_size.h > bounds->size.h) {
|
||||
// We have scrolled up, so we want to display the down arrow.
|
||||
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_DOWN);
|
||||
} else if (offset.y + content_size.h <= bounds->size.h) {
|
||||
// Hide the down arrow as we've reached the bottom.
|
||||
action_bar_layer_clear_icon(action_bar, BUTTON_ID_DOWN);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_up_click_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
ExpandableDialog *expandable_dialog = context;
|
||||
prv_manual_scroll(&expandable_dialog->scroll_layer, 1);
|
||||
}
|
||||
|
||||
static void prv_down_click_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
ExpandableDialog *expandable_dialog = context;
|
||||
prv_manual_scroll(&expandable_dialog->scroll_layer, -1);
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *context) {
|
||||
ExpandableDialog *expandable_dialog = context;
|
||||
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100, prv_up_click_handler);
|
||||
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100, prv_down_click_handler);
|
||||
if (expandable_dialog->select_click_handler) {
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, expandable_dialog->select_click_handler);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_expandable_dialog_load(Window *window) {
|
||||
ExpandableDialog *expandable_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = &expandable_dialog->dialog;
|
||||
|
||||
GRect frame = window->layer.bounds;
|
||||
static const uint16_t ICON_TOP_MARGIN_PX = 16;
|
||||
static const uint16_t BOTTOM_MARGIN_PX = 6;
|
||||
static const uint16_t CONTENT_DOWN_ARROW_HEIGHT = PBL_IF_RECT_ELSE(16, 10);
|
||||
|
||||
// Small margin is shown when we have an action bar to fit more text on the line.
|
||||
const uint16_t SM_LEFT_MARGIN_PX = 4;
|
||||
// Normal margin shown when there is no action bar in the expandable dialog.
|
||||
const uint16_t NM_LEFT_MARGIN_PX = 10;
|
||||
|
||||
bool show_action_bar = expandable_dialog->show_action_bar;
|
||||
|
||||
uint16_t left_margin_px = show_action_bar ? SM_LEFT_MARGIN_PX : NM_LEFT_MARGIN_PX;
|
||||
uint16_t right_margin_px = left_margin_px;
|
||||
|
||||
bool has_header = *expandable_dialog->header ? true : false;
|
||||
|
||||
const GFont header_font = expandable_dialog->header_font;
|
||||
int32_t header_content_height = has_header ? DISP_ROWS : 0;
|
||||
|
||||
uint16_t status_layer_offset = dialog->show_status_layer * STATUS_BAR_LAYER_HEIGHT;
|
||||
uint16_t action_bar_offset = show_action_bar * ACTION_BAR_WIDTH;
|
||||
|
||||
uint16_t x = 0;
|
||||
uint16_t y = 0;
|
||||
uint16_t w = PBL_IF_RECT_ELSE(frame.size.w - action_bar_offset, frame.size.w);
|
||||
uint16_t h = STATUS_BAR_LAYER_HEIGHT;
|
||||
|
||||
if (dialog->show_status_layer) {
|
||||
dialog_add_status_bar_layer(dialog, &GRect(x, y, w, h));
|
||||
}
|
||||
GContext *ctx = graphics_context_get_current_context();
|
||||
|
||||
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
|
||||
KinoReel *icon = dialog_create_icon(dialog);
|
||||
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
|
||||
uint16_t icon_offset = (icon ? ICON_TOP_MARGIN_PX - status_layer_offset : 0);
|
||||
|
||||
|
||||
x = 0;
|
||||
y = status_layer_offset;
|
||||
w = frame.size.w;
|
||||
h = frame.size.h - y;
|
||||
|
||||
ScrollLayer *scroll_layer = &expandable_dialog->scroll_layer;
|
||||
scroll_layer_init(scroll_layer, &GRect(x, y, w, h));
|
||||
layer_add_child(&window->layer, &scroll_layer->layer);
|
||||
|
||||
#if PBL_ROUND
|
||||
uint16_t page_height = scroll_layer->layer.bounds.size.h;
|
||||
#endif
|
||||
|
||||
// Set up the header if this dialog is set to have one.
|
||||
GTextAlignment alignment = PBL_IF_RECT_ELSE(GTextAlignmentLeft,
|
||||
(show_action_bar ?
|
||||
GTextAlignmentRight : GTextAlignmentCenter));
|
||||
uint16_t right_aligned_box_reduction = PBL_IF_RECT_ELSE(0, show_action_bar ? 10 : 0);
|
||||
if (has_header) {
|
||||
const uint16_t HEADER_OFFSET = 6;
|
||||
#if PBL_RECT
|
||||
x = left_margin_px;
|
||||
w = frame.size.w - right_margin_px - left_margin_px - action_bar_offset
|
||||
- right_aligned_box_reduction;
|
||||
#else
|
||||
x = 0;
|
||||
w = frame.size.w - right_margin_px - action_bar_offset - right_aligned_box_reduction;
|
||||
#endif
|
||||
y = icon ? icon_offset + icon_size.h : -HEADER_OFFSET;
|
||||
|
||||
TextLayer *header_layer = &expandable_dialog->header_layer;
|
||||
text_layer_init_with_parameters(header_layer, &GRect(x, y, w, header_content_height),
|
||||
expandable_dialog->header, header_font, dialog->text_color,
|
||||
GColorClear, alignment, GTextOverflowModeWordWrap);
|
||||
// layer must be added immediately to scroll layer for perimeter and paging
|
||||
scroll_layer_add_child(scroll_layer, &header_layer->layer);
|
||||
|
||||
#if PBL_ROUND
|
||||
text_layer_enable_screen_text_flow_and_paging(header_layer, 8);
|
||||
#endif
|
||||
|
||||
// We account for a header that may be larger than the available space
|
||||
// by adjusting our height variable that is passed to the body layer
|
||||
GSize header_size = text_layer_get_content_size(ctx, header_layer);
|
||||
header_size.h += 4; // See PBL-1741
|
||||
header_size.w = w;
|
||||
header_content_height = header_size.h;
|
||||
text_layer_set_size(header_layer, header_size);
|
||||
}
|
||||
|
||||
// Set up the text.
|
||||
const uint16_t TEXT_OFFSET = 6;
|
||||
x = left_margin_px;
|
||||
y = (icon ? icon_offset + icon_size.h : -TEXT_OFFSET) + header_content_height;
|
||||
w = frame.size.w - right_margin_px - left_margin_px - action_bar_offset
|
||||
- right_aligned_box_reduction;
|
||||
h = INT16_MAX; // height is clamped to content size
|
||||
GFont font = fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD);
|
||||
|
||||
TextLayer *text_layer = &dialog->text_layer;
|
||||
text_layer_init_with_parameters(text_layer, &GRect(x, y, w, h), dialog->buffer, font,
|
||||
dialog->text_color, GColorClear,
|
||||
alignment, GTextOverflowModeWordWrap);
|
||||
// layer must be added immediately to scroll layer for perimeter and paging
|
||||
scroll_layer_add_child(scroll_layer, &text_layer->layer);
|
||||
|
||||
#if PBL_ROUND
|
||||
text_layer_set_line_spacing_delta(text_layer, -1);
|
||||
text_layer_enable_screen_text_flow_and_paging(text_layer, 8);
|
||||
#endif
|
||||
|
||||
int32_t text_content_height = text_layer_get_content_size(ctx, text_layer).h;
|
||||
text_content_height += 4; // See PBL-1741
|
||||
text_layer_set_size(text_layer, GSize(w, text_content_height));
|
||||
|
||||
uint16_t scroll_height = icon_offset + icon_size.h +
|
||||
header_content_height + text_content_height + (icon ? BOTTOM_MARGIN_PX : 0);
|
||||
|
||||
scroll_layer_set_content_size(scroll_layer, GSize(frame.size.w, scroll_height));
|
||||
scroll_layer_set_shadow_hidden(scroll_layer, true);
|
||||
scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) {
|
||||
.content_offset_changed_handler = prv_offset_changed_handler
|
||||
});
|
||||
scroll_layer_set_context(scroll_layer, expandable_dialog);
|
||||
|
||||
#if PBL_ROUND
|
||||
scroll_layer_set_paging(scroll_layer, true);
|
||||
#endif
|
||||
|
||||
if (show_action_bar) {
|
||||
// Icons for up and down on the action bar.
|
||||
#ifndef RECOVERY_FW
|
||||
expandable_dialog->up_icon = gbitmap_create_with_resource_system(SYSTEM_APP,
|
||||
RESOURCE_ID_ACTION_BAR_ICON_UP);
|
||||
expandable_dialog->down_icon = gbitmap_create_with_resource_system(SYSTEM_APP,
|
||||
RESOURCE_ID_ACTION_BAR_ICON_DOWN);
|
||||
PBL_ASSERTN(expandable_dialog->down_icon && expandable_dialog->up_icon);
|
||||
#endif
|
||||
|
||||
// Set up the Action bar.
|
||||
ActionBarLayer *action_bar = &expandable_dialog->action_bar;
|
||||
action_bar_layer_init(action_bar);
|
||||
if (expandable_dialog->action_bar_background_color.a != 0) {
|
||||
action_bar_layer_set_background_color(action_bar,
|
||||
expandable_dialog->action_bar_background_color);
|
||||
}
|
||||
if (expandable_dialog->select_icon) {
|
||||
action_bar_layer_set_icon_animated(action_bar, BUTTON_ID_SELECT,
|
||||
expandable_dialog->select_icon, expandable_dialog->show_action_icon_animated);
|
||||
}
|
||||
action_bar_layer_set_context(action_bar, expandable_dialog);
|
||||
action_bar_layer_set_click_config_provider(action_bar, prv_config_provider);
|
||||
action_bar_layer_add_to_window(action_bar, window);
|
||||
} else {
|
||||
window_set_click_config_provider_with_context(window, prv_config_provider, expandable_dialog);
|
||||
}
|
||||
|
||||
x = PBL_IF_RECT_ELSE(left_margin_px, (show_action_bar) ?
|
||||
(frame.size.w - right_margin_px - left_margin_px -
|
||||
action_bar_offset - right_aligned_box_reduction - icon_size.h) :
|
||||
(90 - icon_size.h / 2));
|
||||
y = icon_offset + PBL_IF_RECT_ELSE(0, 5);
|
||||
if (dialog_init_icon_layer(dialog, icon, GPoint(x, y), false /* not animated */)) {
|
||||
scroll_layer_add_child(scroll_layer, &dialog->icon_layer.layer);
|
||||
}
|
||||
|
||||
// Check if we should show the down arrow by checking if we have enough content to warrant
|
||||
// the scroll layer scrolling.
|
||||
if (scroll_height > frame.size.h) {
|
||||
if (show_action_bar) {
|
||||
prv_show_action_bar_icon(expandable_dialog, BUTTON_ID_DOWN);
|
||||
} else {
|
||||
// If there isn't an action bar and there is more content than fits the screen
|
||||
// setup the status layer and content_down_arrow_layer
|
||||
ContentIndicator *indicator = scroll_layer_get_content_indicator(scroll_layer);
|
||||
content_indicator_configure_direction(
|
||||
indicator, ContentIndicatorDirectionUp,
|
||||
&(ContentIndicatorConfig) {
|
||||
.layer = &expandable_dialog->dialog.status_layer.layer,
|
||||
.times_out = true,
|
||||
.colors.foreground = dialog->text_color,
|
||||
.colors.background = dialog->window.background_color,
|
||||
});
|
||||
layer_init(&expandable_dialog->content_down_arrow_layer, &GRect(
|
||||
0, frame.size.h - CONTENT_DOWN_ARROW_HEIGHT,
|
||||
PBL_IF_RECT_ELSE(frame.size.w - action_bar_offset, frame.size.w),
|
||||
CONTENT_DOWN_ARROW_HEIGHT));
|
||||
layer_add_child(&window->layer, &expandable_dialog->content_down_arrow_layer);
|
||||
content_indicator_configure_direction(
|
||||
indicator, ContentIndicatorDirectionDown,
|
||||
&(ContentIndicatorConfig) {
|
||||
.layer = &expandable_dialog->content_down_arrow_layer,
|
||||
.times_out = false,
|
||||
.alignment = PBL_IF_RECT_ELSE(GAlignCenter, GAlignTop),
|
||||
.colors.foreground = dialog->text_color,
|
||||
.colors.background = dialog->window.background_color,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dialog_load(dialog);
|
||||
}
|
||||
|
||||
static void prv_expandable_dialog_appear(Window *window) {
|
||||
ExpandableDialog *expandable_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
|
||||
scroll_layer_update_content_indicator(&expandable_dialog->scroll_layer);
|
||||
dialog_appear(dialog);
|
||||
}
|
||||
|
||||
static void prv_expandable_dialog_unload(Window *window) {
|
||||
ExpandableDialog *expandable_dialog = window_get_user_data(window);
|
||||
dialog_unload(&expandable_dialog->dialog);
|
||||
if (expandable_dialog->show_action_bar) {
|
||||
action_bar_layer_deinit(&expandable_dialog->action_bar);
|
||||
}
|
||||
gbitmap_destroy(expandable_dialog->up_icon);
|
||||
gbitmap_destroy(expandable_dialog->down_icon);
|
||||
if (*expandable_dialog->header) {
|
||||
text_layer_deinit(&expandable_dialog->header_layer);
|
||||
}
|
||||
scroll_layer_deinit(&expandable_dialog->scroll_layer);
|
||||
if (expandable_dialog->dialog.destroy_on_pop) {
|
||||
applib_free(expandable_dialog);
|
||||
}
|
||||
}
|
||||
|
||||
Dialog *expandable_dialog_get_dialog(ExpandableDialog *expandable_dialog) {
|
||||
return &expandable_dialog->dialog;
|
||||
}
|
||||
|
||||
void expandable_dialog_init(ExpandableDialog *expandable_dialog, const char *dialog_name) {
|
||||
PBL_ASSERTN(expandable_dialog);
|
||||
*expandable_dialog = (ExpandableDialog) {
|
||||
.header_font = fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD),
|
||||
};
|
||||
|
||||
dialog_init(&expandable_dialog->dialog, dialog_name);
|
||||
Window *window = &expandable_dialog->dialog.window;
|
||||
window_set_window_handlers(window, &(WindowHandlers) {
|
||||
.load = prv_expandable_dialog_load,
|
||||
.unload = prv_expandable_dialog_unload,
|
||||
.appear = prv_expandable_dialog_appear,
|
||||
});
|
||||
expandable_dialog->show_action_bar = true;
|
||||
window_set_user_data(window, expandable_dialog);
|
||||
}
|
||||
|
||||
ExpandableDialog *expandable_dialog_create(const char *dialog_name) {
|
||||
// Note: Note exported so no padding necessary
|
||||
ExpandableDialog *expandable_dialog = applib_type_malloc(ExpandableDialog);
|
||||
if (expandable_dialog) {
|
||||
expandable_dialog_init(expandable_dialog, dialog_name);
|
||||
}
|
||||
return expandable_dialog;
|
||||
}
|
||||
|
||||
void expandable_dialog_close_cb(ClickRecognizerRef recognizer, void *e_dialog) {
|
||||
expandable_dialog_pop(e_dialog);
|
||||
}
|
||||
|
||||
ExpandableDialog *expandable_dialog_create_with_params(const char *dialog_name, ResourceId icon,
|
||||
const char *text, GColor text_color,
|
||||
GColor background_color,
|
||||
DialogCallbacks *callbacks,
|
||||
ResourceId select_icon,
|
||||
ClickHandler select_click_handler) {
|
||||
ExpandableDialog *expandable_dialog = expandable_dialog_create(dialog_name);
|
||||
if (expandable_dialog) {
|
||||
expandable_dialog_set_select_action(expandable_dialog, select_icon, select_click_handler);
|
||||
|
||||
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
|
||||
dialog_set_icon(dialog, icon);
|
||||
dialog_set_text(dialog, text);
|
||||
dialog_set_background_color(dialog, background_color);
|
||||
dialog_set_text_color(dialog, gcolor_legible_over(background_color));
|
||||
dialog_set_callbacks(dialog, callbacks, dialog);
|
||||
}
|
||||
return expandable_dialog;
|
||||
}
|
||||
|
||||
void expandable_dialog_show_action_bar(ExpandableDialog *expandable_dialog,
|
||||
bool show_action_bar) {
|
||||
expandable_dialog->show_action_bar = show_action_bar;
|
||||
}
|
||||
|
||||
void expandable_dialog_set_action_icon_animated(ExpandableDialog *expandable_dialog,
|
||||
bool animated) {
|
||||
expandable_dialog->show_action_icon_animated = animated;
|
||||
}
|
||||
|
||||
void expandable_dialog_set_action_bar_background_color(ExpandableDialog *expandable_dialog,
|
||||
GColor background_color) {
|
||||
expandable_dialog->action_bar_background_color = background_color;
|
||||
}
|
||||
|
||||
void expandable_dialog_set_header(ExpandableDialog *expandable_dialog, const char *header) {
|
||||
if (!header) {
|
||||
expandable_dialog->header[0] = 0;
|
||||
return;
|
||||
}
|
||||
strncpy(expandable_dialog->header, header, DIALOG_MAX_HEADER_LEN);
|
||||
expandable_dialog->header[DIALOG_MAX_HEADER_LEN] = '\0';
|
||||
}
|
||||
|
||||
void expandable_dialog_set_header_font(ExpandableDialog *expandable_dialog, GFont header_font) {
|
||||
expandable_dialog->header_font = header_font;
|
||||
}
|
||||
|
||||
void expandable_dialog_set_select_action(ExpandableDialog *expandable_dialog,
|
||||
uint32_t resource_id,
|
||||
ClickHandler select_click_handler) {
|
||||
if (expandable_dialog->select_icon) {
|
||||
gbitmap_destroy(expandable_dialog->select_icon);
|
||||
expandable_dialog->select_icon = NULL;
|
||||
}
|
||||
|
||||
if (resource_id != RESOURCE_ID_INVALID) {
|
||||
expandable_dialog->select_icon = gbitmap_create_with_resource_system(SYSTEM_APP, resource_id);
|
||||
}
|
||||
expandable_dialog->select_click_handler = select_click_handler;
|
||||
}
|
||||
|
||||
void expandable_dialog_push(ExpandableDialog *expandable_dialog, WindowStack *window_stack) {
|
||||
dialog_push(&expandable_dialog->dialog, window_stack);
|
||||
}
|
||||
|
||||
void app_expandable_dialog_push(ExpandableDialog *expandable_dialog) {
|
||||
app_dialog_push(&expandable_dialog->dialog);
|
||||
}
|
||||
|
||||
void expandable_dialog_pop(ExpandableDialog *expandable_dialog) {
|
||||
dialog_pop(&expandable_dialog->dialog);
|
||||
}
|
148
src/fw/applib/ui/dialogs/expandable_dialog.h
Normal file
148
src/fw/applib/ui/dialogs/expandable_dialog.h
Normal file
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/ui/click.h"
|
||||
#include "applib/ui/action_bar_layer.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/scroll_layer.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define DIALOG_MAX_HEADER_LEN 30
|
||||
|
||||
// An ExpandableDialog is dialog that contains a large amount of text that can be scrolled. It also
|
||||
// contains an action bar which indicates which directions can currently be scrolled and optionally
|
||||
// a SELECT button action.
|
||||
typedef struct ExpandableDialog {
|
||||
Dialog dialog;
|
||||
|
||||
bool show_action_bar;
|
||||
bool show_action_icon_animated;
|
||||
|
||||
GColor action_bar_background_color;
|
||||
ActionBarLayer action_bar;
|
||||
ClickHandler select_click_handler;
|
||||
|
||||
GBitmap *up_icon;
|
||||
GBitmap *select_icon;
|
||||
GBitmap *down_icon;
|
||||
|
||||
GFont header_font;
|
||||
char header[DIALOG_MAX_HEADER_LEN + 1];
|
||||
|
||||
TextLayer header_layer;
|
||||
ScrollLayer scroll_layer;
|
||||
Layer content_down_arrow_layer;
|
||||
} ExpandableDialog;
|
||||
|
||||
//! Creates a new ExpandableDialog on the heap.
|
||||
//! @param dialog_name The name to give the dialog
|
||||
//! @return Pointer to an \ref ExpandableDialog
|
||||
ExpandableDialog *expandable_dialog_create(const char *dialog_name);
|
||||
|
||||
//! Creates a new ExpandableDialog on the heap with additional parameters
|
||||
//! @param dialog_name The name to give the dialog
|
||||
//! @param icon The icon which appears at the top of the dialog
|
||||
//! @param text The text to display in the dialog
|
||||
//! @param text_color The color of the dialog's text
|
||||
//! @param background_color The background color of the dialog
|
||||
//! @param callbacks The \ref DialogCallbacks to assign to the dialog
|
||||
//! @param select_icon The icon to assign to the select button on the action menu
|
||||
//! @param select_click_handler The \ref ClickHandler to assign to the select button
|
||||
//! @return Pointer to an \ref ExpandableDialog
|
||||
ExpandableDialog *expandable_dialog_create_with_params(const char *dialog_name, ResourceId icon,
|
||||
const char *text, GColor text_color,
|
||||
GColor background_color,
|
||||
DialogCallbacks *callbacks,
|
||||
ResourceId select_icon,
|
||||
ClickHandler select_click_handler);
|
||||
|
||||
//! Simple callback which closes the dialog when called
|
||||
void expandable_dialog_close_cb(ClickRecognizerRef recognizer, void *e_dialog);
|
||||
|
||||
//! Intializes an ExpandableDialog
|
||||
//! @param expandable_dialog Pointer to an \ref ExpandableDialog
|
||||
//! param dialog_name The name to give the \ref ExpandableDialog
|
||||
void expandable_dialog_init(ExpandableDialog *expandable_dialog, const char *dialog_name);
|
||||
|
||||
//! Retrieves the internal Dialog object of the Expandable Dialog.
|
||||
//! @param expandable_dialog The \ref ExpandableDialog to retrieve from.
|
||||
//! @return \ref Dialog
|
||||
Dialog *expandable_dialog_get_dialog(ExpandableDialog *expandable_dialog);
|
||||
|
||||
//! Sets whether or not the expandable dialog should should show its action bar.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to set on
|
||||
//! @param show_action_bar Boolean indicating whether to show the action bar
|
||||
void expandable_dialog_show_action_bar(ExpandableDialog *expandable_dialog,
|
||||
bool show_action_bar);
|
||||
|
||||
//! Sets whether to animate the action bar items.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to set on
|
||||
//! @param animated Boolean indicating whether or not to animate the icons
|
||||
//! @note Unless \ref expandable_dialog_show_action_bar is called with true, this function
|
||||
//! will not have any noticeable change on the \ref ExpandableDialog
|
||||
void expandable_dialog_set_action_icon_animated(ExpandableDialog *expandable_dialog,
|
||||
bool animated);
|
||||
|
||||
//! Sets the action bar background color
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog for which to set
|
||||
//! @param background_color The background color of the dialog's action bar
|
||||
void expandable_dialog_set_action_bar_background_color(ExpandableDialog *expandable_dialog,
|
||||
GColor background_color);
|
||||
|
||||
//! Sets the text of the optional header text. The header has a maximum length of
|
||||
//! \ref DIALOG_MAX_HEADER_LEN and the text passed in will be clipped if it exceeds that
|
||||
//! length.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog on which to set the header
|
||||
//! @param header Text to set as the header.
|
||||
//! @note If set to NULL, the header will not appear.
|
||||
void expandable_dialog_set_header(ExpandableDialog *expandable_dialog, const char *header);
|
||||
|
||||
//! Sets the header font
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog on which to set the header
|
||||
//! @param header_font The font to use for the header text
|
||||
void expandable_dialog_set_header_font(ExpandableDialog *expandable_dialog, GFont header_font);
|
||||
|
||||
//! Sets the icon and ClickHandler of the SELECT button on the action bar.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog for which to set
|
||||
//! @param resource_id The resource id of the resource to be used to create the select bitmap
|
||||
//! @param select_click_handler Handler to call when the select handler is clicked in the
|
||||
//! Expandable Dialog's action bar layer.
|
||||
//! @note Passing \ref RESOURCE_ID_INVALID as the resource_id to the function will allow you
|
||||
//! to set an action with no icon appearing in the \ref ActionBarLayer
|
||||
void expandable_dialog_set_select_action(ExpandableDialog *expandable_dialog,
|
||||
uint32_t resource_id,
|
||||
ClickHandler select_click_handler);
|
||||
|
||||
//! Pushes the dialog onto the window stack.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to push.
|
||||
//! @param window_stack Pointer to the \ref WindowStack to push the dialog to
|
||||
void expandable_dialog_push(ExpandableDialog *expandable_dialog, WindowStack *window_stack);
|
||||
|
||||
//! Pushes the dialog onto the app's window stack
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to push.
|
||||
//! @note: Put a better comment here before exporting
|
||||
void app_expandable_dialog_push(ExpandableDialog *expandable_dialog);
|
||||
|
||||
//! Wrapper for popping the underlying dialog off of the window stack. Useful for when the
|
||||
//! user overrides the default behaviour of the select action to allow them to pop the dialog.
|
||||
//! @param expandable_dialog Pointer to the \ref ExpandableDialog to pop.
|
||||
void expandable_dialog_pop(ExpandableDialog *expandable_dialog);
|
227
src/fw/applib/ui/dialogs/simple_dialog.c
Normal file
227
src/fw/applib/ui/dialogs/simple_dialog.c
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 "simple_dialog.h"
|
||||
|
||||
#include "applib/applib_malloc.auto.h"
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/ui/bitmap_layer.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/dialogs/dialog_private.h"
|
||||
#include "applib/ui/layer.h"
|
||||
#include "applib/ui/text_layer.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "kernel/ui/kernel_ui.h"
|
||||
#include "system/passert.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#if (RECOVERY_FW || UNITTEST)
|
||||
#define SIMPLE_DIALOG_ANIMATED false
|
||||
#else
|
||||
#define SIMPLE_DIALOG_ANIMATED true
|
||||
#endif
|
||||
|
||||
// Layout Defines
|
||||
#define TEXT_ALIGNMENT (GTextAlignmentCenter)
|
||||
#define TEXT_OVERFLOW (GTextOverflowModeWordWrap)
|
||||
#define TEXT_FONT (fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD))
|
||||
|
||||
#define TEXT_LEFT_MARGIN_PX (PBL_IF_RECT_ELSE(6, 0))
|
||||
#define TEXT_RIGHT_MARGIN_PX (PBL_IF_RECT_ELSE(6, 0))
|
||||
#define TEXT_FLOW_INSET_PX (PBL_IF_RECT_ELSE(0, 8))
|
||||
#define TEXT_LINE_HEIGHT_PX (fonts_get_font_height(TEXT_FONT))
|
||||
#define TEXT_MAX_HEIGHT_PX ((2 * TEXT_LINE_HEIGHT_PX) + 8) // 2 line + some space for descenders
|
||||
|
||||
|
||||
static int prv_get_rendered_text_height(const char *text, const GRect *text_box) {
|
||||
GContext *ctx = graphics_context_get_current_context();
|
||||
TextLayoutExtended layout = { 0 };
|
||||
graphics_text_attributes_enable_screen_text_flow((GTextLayoutCacheRef) &layout,
|
||||
TEXT_FLOW_INSET_PX);
|
||||
return graphics_text_layout_get_max_used_size(ctx,
|
||||
text,
|
||||
TEXT_FONT,
|
||||
*text_box,
|
||||
TEXT_OVERFLOW,
|
||||
TEXT_ALIGNMENT,
|
||||
(GTextLayoutCacheRef) &layout).h;
|
||||
}
|
||||
|
||||
static int prv_get_icon_top_margin(bool has_status_bar, int icon_height, int window_height) {
|
||||
const uint16_t status_layer_offset = has_status_bar ? 6 : 0;
|
||||
#if PLATFORM_ROBERT || PLATFORM_CALCULUS
|
||||
const uint16_t icon_top_default_margin_px = 42 + status_layer_offset;
|
||||
#else
|
||||
const uint16_t icon_top_default_margin_px = 18 + status_layer_offset;
|
||||
#endif
|
||||
const uint16_t frame_height_claimed = icon_height + TEXT_MAX_HEIGHT_PX + status_layer_offset;
|
||||
const uint16_t icon_top_adjusted_margin_px = MAX(window_height - frame_height_claimed, 0);
|
||||
// Try and use the default value if possible.
|
||||
return (icon_top_adjusted_margin_px < icon_top_default_margin_px) ? icon_top_adjusted_margin_px :
|
||||
icon_top_default_margin_px;
|
||||
}
|
||||
|
||||
static void prv_get_text_box(GSize frame_size, GSize icon_size,
|
||||
int icon_top_margin_px, GRect *text_box_out) {
|
||||
const uint16_t icon_text_spacing_px = PBL_IF_ROUND_ELSE(2, 4);
|
||||
|
||||
const uint16_t text_x = TEXT_LEFT_MARGIN_PX;
|
||||
const uint16_t text_y = icon_top_margin_px + MAX(icon_size.h, 6) + icon_text_spacing_px;
|
||||
const uint16_t text_w = frame_size.w - TEXT_LEFT_MARGIN_PX - TEXT_RIGHT_MARGIN_PX;
|
||||
// Limit to 2 lines if there is an icon
|
||||
const uint16_t text_h = icon_size.h ? TEXT_MAX_HEIGHT_PX : frame_size.h - text_y;
|
||||
|
||||
*text_box_out = GRect(text_x, text_y, text_w, text_h);
|
||||
}
|
||||
|
||||
static void prv_simple_dialog_load(Window *window) {
|
||||
SimpleDialog *simple_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = &simple_dialog->dialog;
|
||||
|
||||
// Ownership of icon is taken over by KinoLayer in dialog_init_icon_layer() call below
|
||||
KinoReel *icon = dialog_create_icon(dialog);
|
||||
const GSize icon_size = icon ? kino_reel_get_size(icon) : GSizeZero;
|
||||
|
||||
GRect frame = window->layer.bounds;
|
||||
|
||||
// Status Layer
|
||||
if (dialog->show_status_layer) {
|
||||
dialog_add_status_bar_layer(dialog, &GRect(0, 0, frame.size.w, STATUS_BAR_LAYER_HEIGHT));
|
||||
}
|
||||
|
||||
uint16_t icon_top_margin_px = prv_get_icon_top_margin(dialog->show_status_layer,
|
||||
icon_size.h, frame.size.h);
|
||||
|
||||
// Text
|
||||
GRect text_box;
|
||||
prv_get_text_box(frame.size, icon_size, icon_top_margin_px, &text_box);
|
||||
|
||||
const uint16_t text_height = prv_get_rendered_text_height(dialog->buffer, &text_box);
|
||||
|
||||
if (text_height <= TEXT_LINE_HEIGHT_PX) {
|
||||
const int additional_icon_top_offset_for_single_line_text_px = 13;
|
||||
// Move the icon down by increasing the margin to vertically center things
|
||||
icon_top_margin_px += additional_icon_top_offset_for_single_line_text_px;
|
||||
// Move the text down as well to preserve spacing
|
||||
// The -1 is there to preserve prior functionality ¯\_(ツ)_/¯
|
||||
text_box.origin.y += additional_icon_top_offset_for_single_line_text_px - 1;
|
||||
}
|
||||
|
||||
TextLayer *text_layer = &dialog->text_layer;
|
||||
text_layer_init_with_parameters(text_layer, &text_box, dialog->buffer, TEXT_FONT,
|
||||
dialog->text_color, GColorClear, TEXT_ALIGNMENT, TEXT_OVERFLOW);
|
||||
layer_add_child(&window->layer, &text_layer->layer);
|
||||
|
||||
#if PBL_ROUND
|
||||
text_layer_enable_screen_text_flow_and_paging(text_layer, TEXT_FLOW_INSET_PX);
|
||||
#endif
|
||||
|
||||
// Icon
|
||||
const GPoint icon_origin = GPoint((grect_get_max_x(&frame) - icon_size.w) / 2,
|
||||
icon_top_margin_px);
|
||||
|
||||
if (dialog_init_icon_layer(dialog, icon, icon_origin, !simple_dialog->icon_static)) {
|
||||
layer_add_child(&dialog->window.layer, &dialog->icon_layer.layer);
|
||||
}
|
||||
|
||||
dialog_load(dialog);
|
||||
}
|
||||
|
||||
static void prv_simple_dialog_appear(Window *window) {
|
||||
SimpleDialog *simple_dialog = window_get_user_data(window);
|
||||
Dialog *dialog = simple_dialog_get_dialog(simple_dialog);
|
||||
dialog_appear(dialog);
|
||||
}
|
||||
|
||||
static void prv_simple_dialog_unload(Window *window) {
|
||||
SimpleDialog *simple_dialog = window_get_user_data(window);
|
||||
dialog_unload(&simple_dialog->dialog);
|
||||
if (simple_dialog->dialog.destroy_on_pop) {
|
||||
applib_free(simple_dialog);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_click_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
SimpleDialog *simple_dialog = context;
|
||||
if (!simple_dialog->buttons_disabled) {
|
||||
dialog_pop(&simple_dialog->dialog);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_config_provider(void *context) {
|
||||
// Simple dialogs are dimissed when any button is pushed.
|
||||
window_single_click_subscribe(BUTTON_ID_SELECT, prv_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_UP, prv_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_DOWN, prv_click_handler);
|
||||
}
|
||||
|
||||
Dialog *simple_dialog_get_dialog(SimpleDialog *simple_dialog) {
|
||||
return &simple_dialog->dialog;
|
||||
}
|
||||
|
||||
void simple_dialog_push(SimpleDialog *simple_dialog, WindowStack *window_stack) {
|
||||
dialog_push(&simple_dialog->dialog, window_stack);
|
||||
}
|
||||
|
||||
void app_simple_dialog_push(SimpleDialog *simple_dialog) {
|
||||
app_dialog_push(&simple_dialog->dialog);
|
||||
}
|
||||
|
||||
void simple_dialog_init(SimpleDialog *simple_dialog, const char *dialog_name) {
|
||||
PBL_ASSERTN(simple_dialog);
|
||||
*simple_dialog = (SimpleDialog) {
|
||||
.icon_static = !SIMPLE_DIALOG_ANIMATED,
|
||||
};
|
||||
|
||||
dialog_init(&simple_dialog->dialog, dialog_name);
|
||||
Window *window = &simple_dialog->dialog.window;
|
||||
window_set_window_handlers(window, &(WindowHandlers) {
|
||||
.load = prv_simple_dialog_load,
|
||||
.unload = prv_simple_dialog_unload,
|
||||
.appear = prv_simple_dialog_appear,
|
||||
});
|
||||
window_set_click_config_provider_with_context(window, prv_config_provider, simple_dialog);
|
||||
window_set_user_data(window, simple_dialog);
|
||||
}
|
||||
|
||||
SimpleDialog *simple_dialog_create(const char *dialog_name) {
|
||||
SimpleDialog *simple_dialog = applib_malloc(sizeof(SimpleDialog));
|
||||
if (simple_dialog) {
|
||||
simple_dialog_init(simple_dialog, dialog_name);
|
||||
}
|
||||
return simple_dialog;
|
||||
}
|
||||
|
||||
void simple_dialog_set_buttons_enabled(SimpleDialog *simple_dialog, bool enabled) {
|
||||
simple_dialog->buttons_disabled = !enabled;
|
||||
}
|
||||
|
||||
void simple_dialog_set_icon_animated(SimpleDialog *simple_dialog, bool animated) {
|
||||
// This cannot be set after the window has been loaded
|
||||
PBL_ASSERTN(!window_is_loaded(&simple_dialog->dialog.window));
|
||||
simple_dialog->icon_static = !animated;
|
||||
}
|
||||
|
||||
bool simple_dialog_does_text_fit(const char *text, GSize window_size,
|
||||
GSize icon_size, bool has_status_bar) {
|
||||
const uint16_t icon_top_margin_px = prv_get_icon_top_margin(has_status_bar, icon_size.h,
|
||||
window_size.h);
|
||||
GRect text_box;
|
||||
prv_get_text_box(window_size, icon_size, icon_top_margin_px, &text_box);
|
||||
return prv_get_rendered_text_height(text, &text_box) <= TEXT_MAX_HEIGHT_PX;
|
||||
}
|
68
src/fw/applib/ui/dialogs/simple_dialog.h
Normal file
68
src/fw/applib/ui/dialogs/simple_dialog.h
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/perimeter.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/window_stack.h"
|
||||
|
||||
//! Simple dialogs just contain a large icon and some text.
|
||||
//! @internal
|
||||
typedef struct SimpleDialog {
|
||||
Dialog dialog;
|
||||
bool buttons_disabled;
|
||||
bool icon_static;
|
||||
} SimpleDialog;
|
||||
|
||||
//! Creates a new SimpleDialog on the heap.
|
||||
//! @param dialog_name The debug name to give the dialog
|
||||
//! @return Pointer to a \ref SimpleDialog
|
||||
SimpleDialog *simple_dialog_create(const char *dialog_name);
|
||||
|
||||
//! @internal
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog to initialize
|
||||
//! @param dialog_name The debug name to give the dialog
|
||||
void simple_dialog_init(SimpleDialog *simple_dialog, const char *dialog_name);
|
||||
|
||||
//! Retrieves the internal Dialog object from the SimpleDialog.
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog whom's dialog to retrieve
|
||||
//! @return pointer to the underlying dialog of the \ref SimpleDialog
|
||||
Dialog *simple_dialog_get_dialog(SimpleDialog *simple_dialog);
|
||||
|
||||
//! Push the \ref SimpleDialog onto the given window stack.
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog to push onto the window stack
|
||||
//! @param window_stack Pointer to a \ref WindowStack to push the dialog onto
|
||||
void simple_dialog_push(SimpleDialog *simple_dialog, WindowStack *window_stack);
|
||||
|
||||
//! Wrapper to call \ref simple_dialog_push() for an app
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog to push onto the app's window stack
|
||||
//! @note: Put a better comment here before exporting
|
||||
void app_simple_dialog_push(SimpleDialog *simple_dialog);
|
||||
|
||||
//! Disables buttons for a \ref SimpleDialog. Usually used in conjunction with
|
||||
//! \ref dialog_set_timeout()
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog
|
||||
//! @param enabled Boolean expressing whether buttons should be enabled for the dialog
|
||||
void simple_dialog_set_buttons_enabled(SimpleDialog *simple_dialog, bool enabled);
|
||||
|
||||
//! Sets whether the dialog icon is animated
|
||||
//! @param simple_dialog Pointer to a \ref SimpleDialog
|
||||
//! @param animated Whether the icon should animate or not
|
||||
void simple_dialog_set_icon_animated(SimpleDialog *simple_dialog, bool animated);
|
||||
|
||||
bool simple_dialog_does_text_fit(const char *text, GSize window_size,
|
||||
GSize icon_size, bool has_status_bar);
|
Loading…
Add table
Add a link
Reference in a new issue