Import of the watch repository from Pebble

This commit is contained in:
Matthieu Jeanson 2024-12-12 16:43:03 -08:00 committed by Katharine Berry
commit 3b92768480
10334 changed files with 2564465 additions and 0 deletions

View 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;
}

View 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);

View 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;

View 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);
}

View 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);

View 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);
}

View 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);

View 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);
}
}

View 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);

View 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);
}

View 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);

View 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);
}

View 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);

View 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;
}

View 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);