diff --git a/applications/examples/example_number_input/ReadMe.md b/applications/examples/example_number_input/ReadMe.md new file mode 100644 index 000000000..9d5a0a9e5 --- /dev/null +++ b/applications/examples/example_number_input/ReadMe.md @@ -0,0 +1,7 @@ +# Number Input + +Simple keyboard that limits user inputs to a full number (integer). Useful to enforce correct entries without the need of intense validations after a user input. + +Definition of min/max values is required. Numbers are of type int32_t. If negative numbers are allowed withing min - max, an additional button is displayed to switch the sign between + and -. + +It is also possible to define a header text, shown in this example app with the 3 different input options. \ No newline at end of file diff --git a/applications/examples/example_number_input/application.fam b/applications/examples/example_number_input/application.fam new file mode 100644 index 000000000..58cff4496 --- /dev/null +++ b/applications/examples/example_number_input/application.fam @@ -0,0 +1,10 @@ +App( + appid="example_number_input", + name="Example: Number Input", + apptype=FlipperAppType.EXTERNAL, + entry_point="example_number_input", + requires=["gui"], + stack_size=1 * 1024, + fap_icon="example_number_input_10px.png", + fap_category="Examples", +) diff --git a/applications/examples/example_number_input/example_number_input.c b/applications/examples/example_number_input/example_number_input.c new file mode 100644 index 000000000..19d787ef5 --- /dev/null +++ b/applications/examples/example_number_input/example_number_input.c @@ -0,0 +1,79 @@ +#include "example_number_input.h" + +bool example_number_input_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + ExampleNumberInput* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool example_number_input_back_event_callback(void* context) { + furi_assert(context); + ExampleNumberInput* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static ExampleNumberInput* example_number_input_alloc() { + ExampleNumberInput* app = malloc(sizeof(ExampleNumberInput)); + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + + app->scene_manager = scene_manager_alloc(&example_number_input_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, example_number_input_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, example_number_input_back_event_callback); + + app->number_input = number_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + ExampleNumberInputViewIdNumberInput, + number_input_get_view(app->number_input)); + + app->dialog_ex = dialog_ex_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + ExampleNumberInputViewIdShowNumber, + dialog_ex_get_view(app->dialog_ex)); + + app->current_number = 5; + app->min_value = INT32_MIN; + app->max_value = INT32_MAX; + + return app; +} + +static void example_number_input_free(ExampleNumberInput* app) { + furi_assert(app); + + view_dispatcher_remove_view(app->view_dispatcher, ExampleNumberInputViewIdShowNumber); + dialog_ex_free(app->dialog_ex); + + view_dispatcher_remove_view(app->view_dispatcher, ExampleNumberInputViewIdNumberInput); + number_input_free(app->number_input); + + scene_manager_free(app->scene_manager); + view_dispatcher_free(app->view_dispatcher); + + furi_record_close(RECORD_GUI); + app->gui = NULL; + + //Remove whatever is left + free(app); +} + +int32_t example_number_input(void* p) { + UNUSED(p); + ExampleNumberInput* app = example_number_input_alloc(); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + scene_manager_next_scene(app->scene_manager, ExampleNumberInputSceneShowNumber); + + view_dispatcher_run(app->view_dispatcher); + + example_number_input_free(app); + + return 0; +} diff --git a/applications/examples/example_number_input/example_number_input.h b/applications/examples/example_number_input/example_number_input.h new file mode 100644 index 000000000..8d944e6fd --- /dev/null +++ b/applications/examples/example_number_input/example_number_input.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scenes/example_number_input_scene.h" + +typedef struct ExampleNumberInputShowNumber ExampleNumberInputShowNumber; + +typedef enum { + ExampleNumberInputViewIdShowNumber, + ExampleNumberInputViewIdNumberInput, +} ExampleNumberInputViewId; + +typedef struct { + Gui* gui; + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + + NumberInput* number_input; + DialogEx* dialog_ex; + + int32_t current_number; + int32_t min_value; + int32_t max_value; +} ExampleNumberInput; diff --git a/applications/examples/example_number_input/example_number_input_10px.png b/applications/examples/example_number_input/example_number_input_10px.png new file mode 100644 index 000000000..bdb494fcd Binary files /dev/null and b/applications/examples/example_number_input/example_number_input_10px.png differ diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene.c b/applications/examples/example_number_input/scenes/example_number_input_scene.c new file mode 100644 index 000000000..caf77fa8c --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene.c @@ -0,0 +1,30 @@ +#include "example_number_input_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const example_number_input_on_enter_handlers[])(void*) = { +#include "example_number_input_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event, +bool (*const example_number_input_on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "example_number_input_scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit, +void (*const example_number_input_on_exit_handlers[])(void* context) = { +#include "example_number_input_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers example_number_input_scene_handlers = { + .on_enter_handlers = example_number_input_on_enter_handlers, + .on_event_handlers = example_number_input_on_event_handlers, + .on_exit_handlers = example_number_input_on_exit_handlers, + .scene_num = ExampleNumberInputSceneNum, +}; diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene.h b/applications/examples/example_number_input/scenes/example_number_input_scene.h new file mode 100644 index 000000000..49fcd256f --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) ExampleNumberInputScene##id, +typedef enum { +#include "example_number_input_scene_config.h" + ExampleNumberInputSceneNum, +} ExampleNumberInputScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers example_number_input_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "example_number_input_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(prefix, name, id) \ + bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "example_number_input_scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context); +#include "example_number_input_scene_config.h" +#undef ADD_SCENE diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene_config.h b/applications/examples/example_number_input/scenes/example_number_input_scene_config.h new file mode 100644 index 000000000..71acbda52 --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene_config.h @@ -0,0 +1,4 @@ +ADD_SCENE(example_number_input, input_number, InputNumber) +ADD_SCENE(example_number_input, show_number, ShowNumber) +ADD_SCENE(example_number_input, input_max, InputMax) +ADD_SCENE(example_number_input, input_min, InputMin) diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene_input_max.c b/applications/examples/example_number_input/scenes/example_number_input_scene_input_max.c new file mode 100644 index 000000000..7478f58a7 --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene_input_max.c @@ -0,0 +1,39 @@ +#include "../example_number_input.h" + +void example_number_input_scene_input_max_callback(void* context, int32_t number) { + ExampleNumberInput* app = context; + app->max_value = number; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void example_number_input_scene_input_max_on_enter(void* context) { + furi_assert(context); + ExampleNumberInput* app = context; + NumberInput* number_input = app->number_input; + + number_input_set_header_text(number_input, "Enter the maximum value"); + number_input_set_result_callback( + number_input, + example_number_input_scene_input_max_callback, + context, + app->max_value, + app->min_value, + INT32_MAX); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleNumberInputViewIdNumberInput); +} + +bool example_number_input_scene_input_max_on_event(void* context, SceneManagerEvent event) { + ExampleNumberInput* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_previous_scene(app->scene_manager); + return true; + } + return consumed; +} + +void example_number_input_scene_input_max_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene_input_min.c b/applications/examples/example_number_input/scenes/example_number_input_scene_input_min.c new file mode 100644 index 000000000..ad7656232 --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene_input_min.c @@ -0,0 +1,39 @@ +#include "../example_number_input.h" + +void example_number_input_scene_input_min_callback(void* context, int32_t number) { + ExampleNumberInput* app = context; + app->min_value = number; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void example_number_input_scene_input_min_on_enter(void* context) { + furi_assert(context); + ExampleNumberInput* app = context; + NumberInput* number_input = app->number_input; + + number_input_set_header_text(number_input, "Enter the minimum value"); + number_input_set_result_callback( + number_input, + example_number_input_scene_input_min_callback, + context, + app->min_value, + INT32_MIN, + app->max_value); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleNumberInputViewIdNumberInput); +} + +bool example_number_input_scene_input_min_on_event(void* context, SceneManagerEvent event) { + ExampleNumberInput* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + scene_manager_previous_scene(app->scene_manager); + return true; + } + return consumed; +} + +void example_number_input_scene_input_min_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene_input_number.c b/applications/examples/example_number_input/scenes/example_number_input_scene_input_number.c new file mode 100644 index 000000000..d9b1fd52f --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene_input_number.c @@ -0,0 +1,42 @@ +#include "../example_number_input.h" + +void example_number_input_scene_input_number_callback(void* context, int32_t number) { + ExampleNumberInput* app = context; + app->current_number = number; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void example_number_input_scene_input_number_on_enter(void* context) { + furi_assert(context); + ExampleNumberInput* app = context; + NumberInput* number_input = app->number_input; + + char str[50]; + snprintf(str, sizeof(str), "Set Number (%ld - %ld)", app->min_value, app->max_value); + + number_input_set_header_text(number_input, str); + number_input_set_result_callback( + number_input, + example_number_input_scene_input_number_callback, + context, + app->current_number, + app->min_value, + app->max_value); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleNumberInputViewIdNumberInput); +} + +bool example_number_input_scene_input_number_on_event(void* context, SceneManagerEvent event) { + ExampleNumberInput* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { //Back button pressed + scene_manager_previous_scene(app->scene_manager); + return true; + } + return consumed; +} + +void example_number_input_scene_input_number_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/examples/example_number_input/scenes/example_number_input_scene_show_number.c b/applications/examples/example_number_input/scenes/example_number_input_scene_show_number.c new file mode 100644 index 000000000..2afdaf5c1 --- /dev/null +++ b/applications/examples/example_number_input/scenes/example_number_input_scene_show_number.c @@ -0,0 +1,66 @@ +#include "../example_number_input.h" + +static void + example_number_input_scene_confirm_dialog_callback(DialogExResult result, void* context) { + ExampleNumberInput* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, result); +} + +static void example_number_input_scene_update_view(void* context) { + ExampleNumberInput* app = context; + DialogEx* dialog_ex = app->dialog_ex; + + dialog_ex_set_header(dialog_ex, "The number is", 64, 0, AlignCenter, AlignTop); + + static char buffer[12]; //needs static for extended lifetime + + snprintf(buffer, sizeof(buffer), "%ld", app->current_number); + dialog_ex_set_text(dialog_ex, buffer, 64, 29, AlignCenter, AlignCenter); + + dialog_ex_set_left_button_text(dialog_ex, "Min"); + dialog_ex_set_right_button_text(dialog_ex, "Max"); + dialog_ex_set_center_button_text(dialog_ex, "Change"); + + dialog_ex_set_result_callback(dialog_ex, example_number_input_scene_confirm_dialog_callback); + dialog_ex_set_context(dialog_ex, app); +} + +void example_number_input_scene_show_number_on_enter(void* context) { + furi_assert(context); + ExampleNumberInput* app = context; + + example_number_input_scene_update_view(app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleNumberInputViewIdShowNumber); +} + +bool example_number_input_scene_show_number_on_event(void* context, SceneManagerEvent event) { + ExampleNumberInput* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case DialogExResultCenter: + scene_manager_next_scene(app->scene_manager, ExampleNumberInputSceneInputNumber); + consumed = true; + break; + case DialogExResultLeft: + scene_manager_next_scene(app->scene_manager, ExampleNumberInputSceneInputMin); + consumed = true; + break; + case DialogExResultRight: + scene_manager_next_scene(app->scene_manager, ExampleNumberInputSceneInputMax); + consumed = true; + break; + default: + break; + } + } + + return consumed; +} + +void example_number_input_scene_show_number_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/services/gui/application.fam b/applications/services/gui/application.fam index b7dd18baa..b24f5bbb6 100644 --- a/applications/services/gui/application.fam +++ b/applications/services/gui/application.fam @@ -19,6 +19,7 @@ App( "view_holder.h", "modules/button_menu.h", "modules/byte_input.h", + "modules/number_input.h", "modules/popup.h", "modules/text_input.h", "modules/widget.h", diff --git a/applications/services/gui/modules/number_input.c b/applications/services/gui/modules/number_input.c new file mode 100644 index 000000000..777e55747 --- /dev/null +++ b/applications/services/gui/modules/number_input.c @@ -0,0 +1,443 @@ +#include "number_input.h" + +#include +#include +#include + +struct NumberInput { + View* view; +}; + +typedef struct { + const char text; + const size_t x; + const size_t y; +} NumberInputKey; + +typedef struct { + FuriString* header; + FuriString* text_buffer; + + int32_t current_number; + int32_t max_value; + int32_t min_value; + + NumberInputCallback callback; + void* callback_context; + + size_t selected_row; + size_t selected_column; +} NumberInputModel; + +static const size_t keyboard_origin_x = 7; +static const size_t keyboard_origin_y = 31; +static const size_t keyboard_row_count = 2; +static const char enter_symbol = '\r'; +static const char backspace_symbol = '\b'; +static const char sign_symbol = '-'; + +static const NumberInputKey keyboard_keys_row_1[] = { + {'0', 0, 12}, + {'1', 11, 12}, + {'2', 22, 12}, + {'3', 33, 12}, + {'4', 44, 12}, + {backspace_symbol, 103, 4}, +}; + +static const NumberInputKey keyboard_keys_row_2[] = { + {'5', 0, 26}, + {'6', 11, 26}, + {'7', 22, 26}, + {'8', 33, 26}, + {'9', 44, 26}, + {sign_symbol, 55, 17}, + {enter_symbol, 95, 17}, +}; + +static size_t number_input_get_row_size(size_t row_index) { + size_t row_size = 0; + + switch(row_index + 1) { + case 1: + row_size = COUNT_OF(keyboard_keys_row_1); + break; + case 2: + row_size = COUNT_OF(keyboard_keys_row_2); + break; + default: + furi_crash(); + } + + return row_size; +} + +static const NumberInputKey* number_input_get_row(size_t row_index) { + const NumberInputKey* row = NULL; + + switch(row_index + 1) { + case 1: + row = keyboard_keys_row_1; + break; + case 2: + row = keyboard_keys_row_2; + break; + default: + furi_crash(); + } + + return row; +} + +static void number_input_draw_input(Canvas* canvas, NumberInputModel* model) { + const size_t text_x = 8; + const size_t text_y = 25; + + elements_slightly_rounded_frame(canvas, 6, 14, 116, 15); + + canvas_draw_str(canvas, text_x, text_y, furi_string_get_cstr(model->text_buffer)); +} + +static bool number_input_use_sign(NumberInputModel* model) { + //only show sign button if allowed number range needs it + if(model->min_value < 0 && model->max_value >= 0) { + return true; + } + return false; +} + +static void number_input_backspace_cb(NumberInputModel* model) { + size_t text_length = furi_string_utf8_length(model->text_buffer); + if(text_length < 1 || (text_length < 2 && model->current_number <= 0)) { + return; + } + furi_string_set_strn( + model->text_buffer, furi_string_get_cstr(model->text_buffer), text_length - 1); + model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10); +} + +static void number_input_handle_up(NumberInputModel* model) { + if(model->selected_row > 0) { + model->selected_row--; + if(model->selected_column > number_input_get_row_size(model->selected_row) - 1) { + model->selected_column = number_input_get_row_size(model->selected_row) - 1; + } + } +} + +static void number_input_handle_down(NumberInputModel* model) { + if(model->selected_row < keyboard_row_count - 1) { + if(model->selected_column >= number_input_get_row_size(model->selected_row) - 1) { + model->selected_column = number_input_get_row_size(model->selected_row + 1) - 1; + } + model->selected_row += 1; + } + const NumberInputKey* keys = number_input_get_row(model->selected_row); + if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) { + model->selected_column--; + } +} + +static void number_input_handle_left(NumberInputModel* model) { + if(model->selected_column > 0) { + model->selected_column--; + } else { + model->selected_column = number_input_get_row_size(model->selected_row) - 1; + } + const NumberInputKey* keys = number_input_get_row(model->selected_row); + if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) { + model->selected_column--; + } +} + +static void number_input_handle_right(NumberInputModel* model) { + if(model->selected_column < number_input_get_row_size(model->selected_row) - 1) { + model->selected_column++; + } else { + model->selected_column = 0; + } + const NumberInputKey* keys = number_input_get_row(model->selected_row); + if(keys[model->selected_column].text == sign_symbol && !number_input_use_sign(model)) { + model->selected_column++; + } +} + +static bool is_number_too_large(NumberInputModel* model) { + int64_t value = strtoll(furi_string_get_cstr(model->text_buffer), NULL, 10); + if(value > (int64_t)model->max_value) { + return true; + } + return false; +} + +static bool is_number_too_small(NumberInputModel* model) { + int64_t value = strtoll(furi_string_get_cstr(model->text_buffer), NULL, 10); + if(value < (int64_t)model->min_value) { + return true; + } + return false; +} + +static void number_input_sign(NumberInputModel* model) { + int32_t number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10); + if(number == 0 && furi_string_cmp_str(model->text_buffer, "-") != 0) { + furi_string_set_str(model->text_buffer, "-"); + return; + } + number = number * -1; + furi_string_printf(model->text_buffer, "%ld", number); + if(is_number_too_large(model) || is_number_too_small(model)) { + furi_string_printf(model->text_buffer, "%ld", model->current_number); + return; + } + model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10); + if(model->current_number == 0) { + furi_string_set_str(model->text_buffer, ""); //show empty if 0, better for usability + } +} + +static void number_input_add_digit(NumberInputModel* model, char* newChar) { + furi_string_cat_str(model->text_buffer, newChar); + if((model->max_value >= 0 && is_number_too_large(model)) || + (model->min_value < 0 && is_number_too_small(model))) { + //you still need to be able to type invalid numbers in some cases to reach valid numbers on later keypress + furi_string_printf(model->text_buffer, "%ld", model->current_number); + return; + } + model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10); + if(model->current_number == 0) { + furi_string_reset(model->text_buffer); + } +} + +static void number_input_handle_ok(NumberInputModel* model) { + char selected = number_input_get_row(model->selected_row)[model->selected_column].text; + char temp_str[2] = {selected, '\0'}; + if(selected == enter_symbol) { + if(is_number_too_large(model) || is_number_too_small(model)) { + return; //Do nothing if number outside allowed range + } + model->current_number = strtol(furi_string_get_cstr(model->text_buffer), NULL, 10); + model->callback(model->callback_context, model->current_number); + } else if(selected == backspace_symbol) { + number_input_backspace_cb(model); + } else if(selected == sign_symbol) { + number_input_sign(model); + } else { + number_input_add_digit(model, temp_str); + } +} + +static void number_input_view_draw_callback(Canvas* canvas, void* _model) { + NumberInputModel* model = _model; + + number_input_draw_input(canvas, model); + + if(!furi_string_empty(model->header)) { + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 2, 9, furi_string_get_cstr(model->header)); + } + canvas_set_font(canvas, FontKeyboard); + // Draw keyboard + for(size_t row = 0; row < keyboard_row_count; row++) { + const size_t column_count = number_input_get_row_size(row); + const NumberInputKey* keys = number_input_get_row(row); + + for(size_t column = 0; column < column_count; column++) { + if(keys[column].text == sign_symbol && !number_input_use_sign(model)) { + continue; + } + + if(keys[column].text == enter_symbol) { + if(is_number_too_small(model) || is_number_too_large(model)) { + //in some cases you need to be able to type a number smaller/larger than the limits (expl. min = 50, clear all and editor must allow to type 9 and later 0 for 90) + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySaveBlockedSelected_24x11); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySaveBlocked_24x11); + } + } else { + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySaveSelected_24x11); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySave_24x11); + } + } + } else if(keys[column].text == backspace_symbol) { + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspaceSelected_16x9); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeyBackspace_16x9); + } + } else if(keys[column].text == sign_symbol) { + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySignSelected_21x11); + } else { + canvas_draw_icon( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + &I_KeySign_21x11); + } + } else { + if(model->selected_row == row && model->selected_column == column) { + canvas_draw_box( + canvas, + keyboard_origin_x + keys[column].x - 3, + keyboard_origin_y + keys[column].y - 10, + 11, + 13); + canvas_set_color(canvas, ColorWhite); + } + + canvas_draw_glyph( + canvas, + keyboard_origin_x + keys[column].x, + keyboard_origin_y + keys[column].y, + keys[column].text); + canvas_set_color(canvas, ColorBlack); + } + } + } +} + +static bool number_input_view_input_callback(InputEvent* event, void* context) { + furi_assert(context); + NumberInput* number_input = context; + + bool consumed = false; + + // Fetch the model + NumberInputModel* model = view_get_model(number_input->view); + + if(event->type == InputTypeShort || event->type == InputTypeLong || + event->type == InputTypeRepeat) { + consumed = true; + switch(event->key) { + case InputKeyLeft: + number_input_handle_left(model); + break; + case InputKeyRight: + number_input_handle_right(model); + break; + case InputKeyUp: + number_input_handle_up(model); + break; + case InputKeyDown: + number_input_handle_down(model); + break; + case InputKeyOk: + number_input_handle_ok(model); + break; + default: + consumed = false; + break; + } + } + + // commit view + view_commit_model(number_input->view, consumed); + + return consumed; +} + +NumberInput* number_input_alloc(void) { + NumberInput* number_input = malloc(sizeof(NumberInput)); + number_input->view = view_alloc(); + view_set_context(number_input->view, number_input); + view_allocate_model(number_input->view, ViewModelTypeLocking, sizeof(NumberInputModel)); + view_set_draw_callback(number_input->view, number_input_view_draw_callback); + view_set_input_callback(number_input->view, number_input_view_input_callback); + + with_view_model( + number_input->view, + NumberInputModel * model, + { + model->header = furi_string_alloc(); + model->text_buffer = furi_string_alloc(); + }, + true); + + return number_input; +} + +void number_input_free(NumberInput* number_input) { + furi_check(number_input); + with_view_model( + number_input->view, + NumberInputModel * model, + { + furi_string_free(model->header); + furi_string_free(model->text_buffer); + }, + true); + view_free(number_input->view); + free(number_input); +} + +View* number_input_get_view(NumberInput* number_input) { + furi_check(number_input); + return number_input->view; +} + +void number_input_set_result_callback( + NumberInput* number_input, + NumberInputCallback callback, + void* callback_context, + int32_t current_number, + int32_t min_value, + int32_t max_value) { + furi_check(number_input); + + current_number = CLAMP(current_number, max_value, min_value); + + with_view_model( + number_input->view, + NumberInputModel * model, + { + model->callback = callback; + model->callback_context = callback_context; + model->current_number = current_number; + furi_string_printf(model->text_buffer, "%ld", current_number); + model->min_value = min_value; + model->max_value = max_value; + }, + true); +} + +void number_input_set_header_text(NumberInput* number_input, const char* text) { + furi_check(number_input); + with_view_model( + number_input->view, + NumberInputModel * model, + { furi_string_set(model->header, text); }, + true); +} diff --git a/applications/services/gui/modules/number_input.h b/applications/services/gui/modules/number_input.h new file mode 100644 index 000000000..80e631e9b --- /dev/null +++ b/applications/services/gui/modules/number_input.h @@ -0,0 +1,69 @@ +/** + * @file number_input.h + * GUI: Integer string keyboard view module API + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Number input anonymous structure */ +typedef struct NumberInput NumberInput; + +/** Callback to be called on save button press */ +typedef void (*NumberInputCallback)(void* context, int32_t number); + +/** Allocate and initialize Number input. + * + * This Number input is used to enter Numbers (Integers). + * + * @return NumberInput instance pointer + */ +NumberInput* number_input_alloc(void); + +/** Deinitialize and free byte input + * + * @param number_input Number input instance + */ +void number_input_free(NumberInput* number_input); + +/** Get byte input view + * + * @param number_input byte input instance + * + * @return View instance that can be used for embedding + */ +View* number_input_get_view(NumberInput* number_input); + +/** Set byte input result callback + * + * @param number_input byte input instance + * @param input_callback input callback fn + * @param callback_context callback context + * @param[in] current_number The current number + * @param min_value Min number value + * @param max_value Max number value + */ + +void number_input_set_result_callback( + NumberInput* number_input, + NumberInputCallback input_callback, + void* callback_context, + int32_t current_number, + int32_t min_value, + int32_t max_value); + +/** Set byte input header text + * + * @param number_input byte input instance + * @param text text to be shown + */ +void number_input_set_header_text(NumberInput* number_input, const char* text); + +#ifdef __cplusplus +} +#endif diff --git a/assets/icons/Keyboard/KeySaveBlockedSelected_24x11.png b/assets/icons/Keyboard/KeySaveBlockedSelected_24x11.png new file mode 100644 index 000000000..fe570a10c Binary files /dev/null and b/assets/icons/Keyboard/KeySaveBlockedSelected_24x11.png differ diff --git a/assets/icons/Keyboard/KeySaveBlocked_24x11.png b/assets/icons/Keyboard/KeySaveBlocked_24x11.png new file mode 100644 index 000000000..d67b294ad Binary files /dev/null and b/assets/icons/Keyboard/KeySaveBlocked_24x11.png differ diff --git a/assets/icons/Keyboard/KeySaveSelected_24x11.png b/assets/icons/Keyboard/KeySaveSelected_24x11.png index 25bc446e4..abfced776 100644 Binary files a/assets/icons/Keyboard/KeySaveSelected_24x11.png and b/assets/icons/Keyboard/KeySaveSelected_24x11.png differ diff --git a/assets/icons/Keyboard/KeySave_24x11.png b/assets/icons/Keyboard/KeySave_24x11.png index 328b726a5..f97838d95 100644 Binary files a/assets/icons/Keyboard/KeySave_24x11.png and b/assets/icons/Keyboard/KeySave_24x11.png differ diff --git a/assets/icons/Keyboard/KeySignSelected_21x11.png b/assets/icons/Keyboard/KeySignSelected_21x11.png new file mode 100644 index 000000000..23ec2a9c4 Binary files /dev/null and b/assets/icons/Keyboard/KeySignSelected_21x11.png differ diff --git a/assets/icons/Keyboard/KeySign_21x11.png b/assets/icons/Keyboard/KeySign_21x11.png new file mode 100644 index 000000000..f31e9e0fa Binary files /dev/null and b/assets/icons/Keyboard/KeySign_21x11.png differ diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index baf1f8a80..69d45e868 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,72.0,, +Version,+,72.1,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, Header,+,applications/services/cli/cli.h,, @@ -13,6 +13,7 @@ Header,+,applications/services/gui/icon_i.h,, Header,+,applications/services/gui/modules/button_menu.h,, Header,+,applications/services/gui/modules/button_panel.h,, Header,+,applications/services/gui/modules/byte_input.h,, +Header,+,applications/services/gui/modules/number_input.h,, Header,+,applications/services/gui/modules/dialog_ex.h,, Header,+,applications/services/gui/modules/empty_screen.h,, Header,+,applications/services/gui/modules/file_browser.h,, @@ -722,6 +723,11 @@ Function,+,byte_input_free,void,ByteInput* Function,+,byte_input_get_view,View*,ByteInput* Function,+,byte_input_set_header_text,void,"ByteInput*, const char*" Function,+,byte_input_set_result_callback,void,"ByteInput*, ByteInputCallback, ByteChangedCallback, void*, uint8_t*, uint8_t" +Function,+,number_input_alloc,NumberInput*, +Function,+,number_input_free,void,NumberInput* +Function,+,number_input_get_view,View*,NumberInput* +Function,+,number_input_set_header_text,void,"NumberInput*, const char*" +Function,+,number_input_set_result_callback,void,"NumberInput*, NumberInputCallback, void*, int32_t, int32_t, int32_t" Function,-,bzero,void,"void*, size_t" Function,+,calloc,void*,"size_t, size_t" Function,+,canvas_clear,void,Canvas* diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index cb3471e60..991569a29 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,72.0,, +Version,+,72.1,, Header,+,applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h,, Header,+,applications/services/bt/bt_service/bt.h,, Header,+,applications/services/bt/bt_service/bt_keys_storage.h,, @@ -20,6 +20,7 @@ Header,+,applications/services/gui/modules/file_browser.h,, Header,+,applications/services/gui/modules/file_browser_worker.h,, Header,+,applications/services/gui/modules/loading.h,, Header,+,applications/services/gui/modules/menu.h,, +Header,+,applications/services/gui/modules/number_input.h,, Header,+,applications/services/gui/modules/popup.h,, Header,+,applications/services/gui/modules/submenu.h,, Header,+,applications/services/gui/modules/text_box.h,, @@ -2815,6 +2816,11 @@ Function,+,notification_internal_message_block,void,"NotificationApp*, const Not Function,+,notification_message,void,"NotificationApp*, const NotificationSequence*" Function,+,notification_message_block,void,"NotificationApp*, const NotificationSequence*" Function,-,nrand48,long,unsigned short[3] +Function,+,number_input_alloc,NumberInput*, +Function,+,number_input_free,void,NumberInput* +Function,+,number_input_get_view,View*,NumberInput* +Function,+,number_input_set_header_text,void,"NumberInput*, const char*" +Function,+,number_input_set_result_callback,void,"NumberInput*, NumberInputCallback, void*, int32_t, int32_t, int32_t" Function,-,on_exit,int,"void (*)(int, void*), void*" Function,+,onewire_host_alloc,OneWireHost*,const GpioPin* Function,+,onewire_host_free,void,OneWireHost*