From 65cd636f500d6a7205a7e9d6ccc109844dc30825 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Mon, 8 Sep 2025 03:12:50 -0700 Subject: [PATCH 1/5] Update Date/Time Input GUI module Add an option to control which fields can be edited by the user --- .../services/gui/modules/date_time_input.c | 114 +++++++++++++++--- .../services/gui/modules/date_time_input.h | 19 +++ targets/f7/api_symbols.csv | 1 + 3 files changed, 114 insertions(+), 20 deletions(-) diff --git a/applications/services/gui/modules/date_time_input.c b/applications/services/gui/modules/date_time_input.c index 63f648edb..abcb16887 100644 --- a/applications/services/gui/modules/date_time_input.c +++ b/applications/services/gui/modules/date_time_input.c @@ -2,12 +2,12 @@ #include #include -#define get_state(m, r, c) \ - ((m)->row == (r) && (m)->column == (c) ? \ - ((m)->editing ? EditStateActiveEditing : EditStateActive) : \ - EditStateNone) - -#define ROW_0_Y (10) +#define get_state(m, r, c, f) \ + ((m)->editable.f ? ((m)->row == (r) && (m)->column == (c) ? \ + ((m)->editing ? EditStateActiveEditing : EditStateActive) : \ + EditStateNone) : \ + EditStateDisabled) +#define ROW_0_Y (9) #define ROW_0_H (20) #define ROW_1_Y (40) @@ -28,6 +28,15 @@ typedef struct { uint8_t column; bool editing; + struct { + bool year; + bool month; + bool day; + bool hour; + bool minute; + bool second; + } editable; + DateTimeChangedCallback changed_callback; DateTimeDoneCallback done_callback; void* callback_context; @@ -37,6 +46,7 @@ typedef enum { EditStateNone, EditStateActive, EditStateActiveEditing, + EditStateDisabled } EditState; static inline void date_time_input_cleanup_date(DateTime* dt) { @@ -59,15 +69,17 @@ static inline void date_time_input_draw_block( furi_assert(text); canvas_set_color(canvas, ColorBlack); - if(state != EditStateNone) { - if(state == EditStateActiveEditing) { - canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5); - canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5); + if(state != EditStateDisabled) { + if(state != EditStateNone) { + if(state == EditStateActiveEditing) { + canvas_draw_icon(canvas, x + w / 2 - 2, y - 1 - 3, &I_SmallArrowUp_3x5); + canvas_draw_icon(canvas, x + w / 2 - 2, y + h + 1, &I_SmallArrowDown_3x5); + } + canvas_draw_rbox(canvas, x, y, w, h, 1); + canvas_set_color(canvas, ColorWhite); + } else { + canvas_draw_rframe(canvas, x, y, w, h, 1); } - canvas_draw_rbox(canvas, x, y, w, h, 1); - canvas_set_color(canvas, ColorWhite); - } else { - canvas_draw_rframe(canvas, x, y, w, h, 1); } canvas_set_font(canvas, font); @@ -88,19 +100,19 @@ static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputMode snprintf(buffer, sizeof(buffer), "%02u", model->datetime->hour); date_time_input_draw_block( - canvas, 30, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 0), buffer); + canvas, 30, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); snprintf(buffer, sizeof(buffer), "%02u", model->datetime->minute); date_time_input_draw_block( - canvas, 64, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 1), buffer); + canvas, 64, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 1, minute), buffer); canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7, 2, 2); canvas_draw_box(canvas, 94, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); snprintf(buffer, sizeof(buffer), "%02u", model->datetime->second); date_time_input_draw_block( - canvas, 98, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 2), buffer); + canvas, 98, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 2, second), buffer); } static void date_time_input_draw_date_callback(Canvas* canvas, DateTimeInputModel* model) { @@ -113,14 +125,14 @@ static void date_time_input_draw_date_callback(Canvas* canvas, DateTimeInputMode canvas_set_font(canvas, FontPrimary); snprintf(buffer, sizeof(buffer), "%04u", model->datetime->year); date_time_input_draw_block( - canvas, 2, ROW_0_Y, 56, ROW_0_H, FontBigNumbers, get_state(model, 0, 0), buffer); + canvas, 2, ROW_0_Y, 56, ROW_0_H, FontBigNumbers, get_state(model, 0, 0, year), buffer); snprintf(buffer, sizeof(buffer), "%02u", model->datetime->month); date_time_input_draw_block( - canvas, 64, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1), buffer); + canvas, 64, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 1, month), buffer); canvas_draw_box(canvas, 64 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2); snprintf(buffer, sizeof(buffer), "%02u", model->datetime->day); date_time_input_draw_block( - canvas, 98, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2), buffer); + canvas, 98, ROW_0_Y, 28, ROW_0_H, FontBigNumbers, get_state(model, 0, 2, day), buffer); canvas_draw_box(canvas, 98 - 5, ROW_0_Y + (ROW_0_H / 2), 4, 2); } @@ -131,17 +143,36 @@ static void date_time_input_view_draw_callback(Canvas* canvas, void* _model) { date_time_input_draw_date_callback(canvas, model); } +static inline bool is_allowed_to_edit(DateTimeInputModel* model) { + return (model->row == 0 && ((model->column == 0 && model->editable.year) | + (model->column == 1 && model->editable.month) | + (model->column == 2 && model->editable.day))) || + ((model->row == 1) && ((model->column == 0 && model->editable.hour) | + (model->column == 1 && model->editable.minute) | + (model->column == 2 && model->editable.second))); +} + static bool date_time_input_navigation_callback(InputEvent* event, DateTimeInputModel* model) { if(event->key == InputKeyUp) { if(model->row > 0) model->row--; + if(!is_allowed_to_edit(model)) model->row++; } else if(event->key == InputKeyDown) { if(model->row < ROW_COUNT - 1) model->row++; + if(!is_allowed_to_edit(model)) model->row--; } else if(event->key == InputKeyOk) { model->editing = !model->editing; } else if(event->key == InputKeyRight) { if(model->column < COLUMN_COUNT - 1) model->column++; + while(model->column < COLUMN_COUNT - 1 && !is_allowed_to_edit(model)) + model->column++; + while(model->column > 0 && !is_allowed_to_edit(model)) + model->column--; } else if(event->key == InputKeyLeft) { if(model->column > 0) model->column--; + while(model->column > 0 && !is_allowed_to_edit(model)) + model->column--; + while(model->column < COLUMN_COUNT - 1 && !is_allowed_to_edit(model)) + model->column++; } else if(event->key == InputKeyBack && model->editing) { model->editing = false; } else if(event->key == InputKeyBack && model->done_callback) { @@ -283,6 +314,13 @@ static void date_time_input_reset_model_input_data(DateTimeInputModel* model) { model->column = 0; model->datetime = NULL; + + model->editable.year = true; + model->editable.month = true; + model->editable.day = true; + model->editable.hour = true; + model->editable.minute = true; + model->editable.second = true; } DateTimeInput* date_time_input_alloc(void) { @@ -345,3 +383,39 @@ void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* with_view_model( date_time_input->view, DateTimeInputModel * model, { model->header = text; }, true); } + +void date_time_input_set_editable_fields( + DateTimeInput* date_time_input, + bool year, + bool month, + bool day, + bool hour, + bool minute, + bool second) { + furi_check(date_time_input); + + with_view_model( + date_time_input->view, + DateTimeInputModel * model, + { + model->editable.year = year; + model->editable.month = month; + model->editable.day = day; + model->editable.hour = hour; + model->editable.minute = minute; + model->editable.second = second; + + // Select first editable field + model->row = 0; + model->column = 0; + while(!is_allowed_to_edit(model)) { + // Cycle to next column and wrap around at end + model->column = (model->column + 1) % COLUMN_COUNT; + // If the column is 0, we wrapped, so go to next row + if(model->column == 0) model->row++; + // If we passed the last row, give up + if(model->row >= ROW_COUNT) break; + }; + }, + true); +} diff --git a/applications/services/gui/modules/date_time_input.h b/applications/services/gui/modules/date_time_input.h index 3efc6c7a1..2969994fd 100644 --- a/applications/services/gui/modules/date_time_input.h +++ b/applications/services/gui/modules/date_time_input.h @@ -65,6 +65,25 @@ void date_time_input_set_result_callback( */ void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text); +/** Set date/time fields which can be edited + * + * @param date_time_input date/time input instance + * @param year whether to allow editing the year + * @param month whether to allow editing the month + * @param day whether to allow editing the day + * @param hour whether to allow editing the hour + * @param minute whether to allow editing the minute + * @param second whether to allow editing the second + */ +void date_time_input_set_editable_fields( + DateTimeInput* date_time_input, + bool year, + bool month, + bool day, + bool hour, + bool minute, + bool second); + #ifdef __cplusplus } #endif diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index 1ea105e00..fcf8388f1 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -929,6 +929,7 @@ Function,-,cuserid,char*,char* Function,+,date_time_input_alloc,DateTimeInput*, Function,+,date_time_input_free,void,DateTimeInput* Function,+,date_time_input_get_view,View*,DateTimeInput* +Function,+,date_time_input_set_editable_fields,void,"DateTimeInput*, _Bool, _Bool, _Bool, _Bool, _Bool, _Bool" Function,+,date_time_input_set_header_text,void,"DateTimeInput*, const char*" Function,+,date_time_input_set_result_callback,void,"DateTimeInput*, DateTimeChangedCallback, DateTimeDoneCallback, void*, DateTime*" Function,+,datetime_datetime_to_timestamp,uint32_t,DateTime* From 8e0f7cfb725b874baede31e8d7e28cffe04cba13 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 23 Sep 2025 10:15:55 -0700 Subject: [PATCH 2/5] Add usage example app --- .../example_date_time_input/ReadMe.md | 13 +++ .../example_date_time_input/application.fam | 9 +++ .../example_date_time_input.c | 79 ++++++++++++++++++ .../example_date_time_input.h | 36 +++++++++ .../scenes/example_date_time_input_scene.c | 31 +++++++ .../scenes/example_date_time_input_scene.h | 29 +++++++ .../example_date_time_input_scene_config.h | 2 + ...le_date_time_input_scene_input_date_time.c | 47 +++++++++++ ...ple_date_time_input_scene_show_date_time.c | 81 +++++++++++++++++++ .../services/gui/modules/date_time_input.c | 9 --- .../services/gui/modules/date_time_input.h | 7 -- targets/f7/api_symbols.csv | 3 +- 12 files changed, 328 insertions(+), 18 deletions(-) create mode 100644 applications/examples/example_date_time_input/ReadMe.md create mode 100644 applications/examples/example_date_time_input/application.fam create mode 100644 applications/examples/example_date_time_input/example_date_time_input.c create mode 100644 applications/examples/example_date_time_input/example_date_time_input.h create mode 100644 applications/examples/example_date_time_input/scenes/example_date_time_input_scene.c create mode 100644 applications/examples/example_date_time_input/scenes/example_date_time_input_scene.h create mode 100644 applications/examples/example_date_time_input/scenes/example_date_time_input_scene_config.h create mode 100644 applications/examples/example_date_time_input/scenes/example_date_time_input_scene_input_date_time.c create mode 100644 applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c diff --git a/applications/examples/example_date_time_input/ReadMe.md b/applications/examples/example_date_time_input/ReadMe.md new file mode 100644 index 000000000..b153965cc --- /dev/null +++ b/applications/examples/example_date_time_input/ReadMe.md @@ -0,0 +1,13 @@ +# Date/Time Input {#example_date_time_input} + +Simple view that allows the user to adjust a date and/or time. + +## Source code + +Source code for this example can be found [here](https://github.com/flipperdevices/flipperzero-firmware/tree/dev/applications/examples/example_date_time_input). + +## General principle + +Callbacks can be defined for every time a value is edited (useful for application-specific bounds checking or validation) and for when the user is done editing (back button is pressed). The provided DateTime object is used both as the initial value and as the place where the result is stored. + +The fields which the user is allowed to edit can be defined using `date_time_input_set_editable_fields()`. Disabled fields are shown but aren't able to be selected and don't have an outer box. If all fields are disabled, the view is read-only and no cursor will be shown. diff --git a/applications/examples/example_date_time_input/application.fam b/applications/examples/example_date_time_input/application.fam new file mode 100644 index 000000000..7f6435840 --- /dev/null +++ b/applications/examples/example_date_time_input/application.fam @@ -0,0 +1,9 @@ +App( + appid="example_date_time_input", + name="Example: Date/Time Input", + apptype=FlipperAppType.EXTERNAL, + entry_point="example_date_time_input", + requires=["gui"], + stack_size=1 * 1024, + fap_category="Examples", +) diff --git a/applications/examples/example_date_time_input/example_date_time_input.c b/applications/examples/example_date_time_input/example_date_time_input.c new file mode 100644 index 000000000..7510f5c52 --- /dev/null +++ b/applications/examples/example_date_time_input/example_date_time_input.c @@ -0,0 +1,79 @@ +#include "example_date_time_input.h" + +bool example_date_time_input_custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + ExampleDateTimeInput* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool example_date_time_input_back_event_callback(void* context) { + furi_assert(context); + ExampleDateTimeInput* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static ExampleDateTimeInput* example_date_time_input_alloc() { + ExampleDateTimeInput* app = malloc(sizeof(ExampleDateTimeInput)); + app->gui = furi_record_open(RECORD_GUI); + + app->view_dispatcher = view_dispatcher_alloc(); + + app->scene_manager = scene_manager_alloc(&example_date_time_input_scene_handlers, app); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback( + app->view_dispatcher, example_date_time_input_custom_event_callback); + view_dispatcher_set_navigation_event_callback( + app->view_dispatcher, example_date_time_input_back_event_callback); + + app->date_time_input = date_time_input_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + ExampleDateTimeInputViewIdDateTimeInput, + date_time_input_get_view(app->date_time_input)); + + app->dialog_ex = dialog_ex_alloc(); + view_dispatcher_add_view( + app->view_dispatcher, + ExampleDateTimeInputViewIdShowDateTime, + dialog_ex_get_view(app->dialog_ex)); + + // Fill in current date & time + furi_hal_rtc_get_datetime(&app->date_time); + app->edit_date = false; + app->edit_time = false; + + return app; +} + +static void example_date_time_input_free(ExampleDateTimeInput* app) { + furi_assert(app); + + view_dispatcher_remove_view(app->view_dispatcher, ExampleDateTimeInputViewIdShowDateTime); + dialog_ex_free(app->dialog_ex); + + view_dispatcher_remove_view(app->view_dispatcher, ExampleDateTimeInputViewIdDateTimeInput); + date_time_input_free(app->date_time_input); + + scene_manager_free(app->scene_manager); + view_dispatcher_free(app->view_dispatcher); + + furi_record_close(RECORD_GUI); + app->gui = NULL; + + free(app); +} + +int32_t example_date_time_input(void* p) { + UNUSED(p); + ExampleDateTimeInput* app = example_date_time_input_alloc(); + + view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen); + + scene_manager_next_scene(app->scene_manager, ExampleDateTimeInputSceneShowDateTime); + + view_dispatcher_run(app->view_dispatcher); + + example_date_time_input_free(app); + + return 0; +} diff --git a/applications/examples/example_date_time_input/example_date_time_input.h b/applications/examples/example_date_time_input/example_date_time_input.h new file mode 100644 index 000000000..6363535f9 --- /dev/null +++ b/applications/examples/example_date_time_input/example_date_time_input.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scenes/example_date_time_input_scene.h" + +typedef struct ExampleDateTimeInputShowDateTime ExampleDateTimeInputShowDateTime; + +typedef enum { + ExampleDateTimeInputViewIdShowDateTime, + ExampleDateTimeInputViewIdDateTimeInput, +} ExampleDateTimeInputViewId; + +typedef struct { + Gui* gui; + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + + DateTimeInput* date_time_input; + DialogEx* dialog_ex; + + DateTime date_time; + + bool edit_date; + bool edit_time; +} ExampleDateTimeInput; diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.c b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.c new file mode 100644 index 000000000..ed3f538f2 --- /dev/null +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.c @@ -0,0 +1,31 @@ +#include "example_date_time_input_scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter, +void (*const example_date_time_input_on_enter_handlers[])(void*) = { +#include "example_date_time_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_date_time_input_on_event_handlers[])(void* context, SceneManagerEvent event) = + { +#include "example_date_time_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_date_time_input_on_exit_handlers[])(void* context) = { +#include "example_date_time_input_scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers example_date_time_input_scene_handlers = { + .on_enter_handlers = example_date_time_input_on_enter_handlers, + .on_event_handlers = example_date_time_input_on_event_handlers, + .on_exit_handlers = example_date_time_input_on_exit_handlers, + .scene_num = ExampleDateTimeInputSceneNum, +}; diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.h b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.h new file mode 100644 index 000000000..5664bad3d --- /dev/null +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(prefix, name, id) ExampleDateTimeInputScene##id, +typedef enum { +#include "example_date_time_input_scene_config.h" + ExampleDateTimeInputSceneNum, +} ExampleDateTimeInputScene; +#undef ADD_SCENE + +extern const SceneManagerHandlers example_date_time_input_scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*); +#include "example_date_time_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_date_time_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_date_time_input_scene_config.h" +#undef ADD_SCENE diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_config.h b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_config.h new file mode 100644 index 000000000..db3b128da --- /dev/null +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_config.h @@ -0,0 +1,2 @@ +ADD_SCENE(example_date_time_input, input_date_time, InputDateTime) +ADD_SCENE(example_date_time_input, show_date_time, ShowDateTime) diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_input_date_time.c b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_input_date_time.c new file mode 100644 index 000000000..8a1288c38 --- /dev/null +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_input_date_time.c @@ -0,0 +1,47 @@ +#include "../example_date_time_input.h" + +void example_date_time_input_scene_input_date_time_callback(void* context) { + ExampleDateTimeInput* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, 0); +} + +void example_date_time_input_scene_input_date_time_on_enter(void* context) { + furi_assert(context); + ExampleDateTimeInput* app = context; + DateTimeInput* date_time_input = app->date_time_input; + + date_time_input_set_result_callback( + date_time_input, + NULL, + example_date_time_input_scene_input_date_time_callback, + context, + &app->date_time); + + date_time_input_set_editable_fields( + date_time_input, + + app->edit_date, + app->edit_date, + app->edit_date, + + app->edit_time, + app->edit_time, + app->edit_time); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleDateTimeInputViewIdDateTimeInput); +} + +bool example_date_time_input_scene_input_date_time_on_event(void* context, SceneManagerEvent event) { + ExampleDateTimeInput* 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_date_time_input_scene_input_date_time_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c new file mode 100644 index 000000000..666db14a6 --- /dev/null +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c @@ -0,0 +1,81 @@ +#include "../example_date_time_input.h" + +static void + example_date_time_input_scene_confirm_dialog_callback(DialogExResult result, void* context) { + ExampleDateTimeInput* app = context; + + view_dispatcher_send_custom_event(app->view_dispatcher, result); +} + +static void example_date_time_input_scene_update_view(void* context) { + ExampleDateTimeInput* app = context; + DialogEx* dialog_ex = app->dialog_ex; + + dialog_ex_set_header(dialog_ex, "The date and time are", 64, 0, AlignCenter, AlignTop); + + char buffer[26] = {}; + snprintf( + buffer, + sizeof(buffer), + "%04d-%02d-%02d\n%02d:%02d:%02d", + app->date_time.year, + app->date_time.month, + app->date_time.day, + app->date_time.hour, + app->date_time.minute, + app->date_time.second); + dialog_ex_set_text(dialog_ex, buffer, 64, 29, AlignCenter, AlignCenter); + + dialog_ex_set_left_button_text(dialog_ex, "Date"); + dialog_ex_set_right_button_text(dialog_ex, "Time"); + dialog_ex_set_center_button_text(dialog_ex, "Both"); + + dialog_ex_set_result_callback( + dialog_ex, example_date_time_input_scene_confirm_dialog_callback); + dialog_ex_set_context(dialog_ex, app); +} + +void example_date_time_input_scene_show_date_time_on_enter(void* context) { + furi_assert(context); + ExampleDateTimeInput* app = context; + + example_date_time_input_scene_update_view(app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ExampleDateTimeInputViewIdShowDateTime); +} + +bool example_date_time_input_scene_show_date_time_on_event(void* context, SceneManagerEvent event) { + ExampleDateTimeInput* app = context; + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + switch(event.event) { + case DialogExResultCenter: + app->edit_date = true; + app->edit_time = true; + scene_manager_next_scene(app->scene_manager, ExampleDateTimeInputSceneInputDateTime); + consumed = true; + break; + case DialogExResultLeft: + app->edit_date = true; + app->edit_time = false; + scene_manager_next_scene(app->scene_manager, ExampleDateTimeInputSceneInputDateTime); + consumed = true; + break; + case DialogExResultRight: + app->edit_date = false; + app->edit_time = true; + scene_manager_next_scene(app->scene_manager, ExampleDateTimeInputSceneInputDateTime); + consumed = true; + break; + default: + break; + } + } + + return consumed; +} + +void example_date_time_input_scene_show_date_time_on_exit(void* context) { + UNUSED(context); +} diff --git a/applications/services/gui/modules/date_time_input.c b/applications/services/gui/modules/date_time_input.c index abcb16887..eb703cda2 100644 --- a/applications/services/gui/modules/date_time_input.c +++ b/applications/services/gui/modules/date_time_input.c @@ -21,7 +21,6 @@ struct DateTimeInput { }; typedef struct { - const char* header; DateTime* datetime; uint8_t row; @@ -335,7 +334,6 @@ DateTimeInput* date_time_input_alloc(void) { date_time_input->view, DateTimeInputModel * model, { - model->header = ""; model->changed_callback = NULL; model->callback_context = NULL; date_time_input_reset_model_input_data(model); @@ -377,13 +375,6 @@ void date_time_input_set_result_callback( true); } -void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text) { - furi_check(date_time_input); - - with_view_model( - date_time_input->view, DateTimeInputModel * model, { model->header = text; }, true); -} - void date_time_input_set_editable_fields( DateTimeInput* date_time_input, bool year, diff --git a/applications/services/gui/modules/date_time_input.h b/applications/services/gui/modules/date_time_input.h index 2969994fd..1c8cdd779 100644 --- a/applications/services/gui/modules/date_time_input.h +++ b/applications/services/gui/modules/date_time_input.h @@ -58,13 +58,6 @@ void date_time_input_set_result_callback( void* callback_context, DateTime* datetime); -/** Set date/time input header text - * - * @param date_time_input date/time input instance - * @param text text to be shown - */ -void date_time_input_set_header_text(DateTimeInput* date_time_input, const char* text); - /** Set date/time fields which can be edited * * @param date_time_input date/time input instance diff --git a/targets/f7/api_symbols.csv b/targets/f7/api_symbols.csv index fcf8388f1..77f6f7e6f 100644 --- a/targets/f7/api_symbols.csv +++ b/targets/f7/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,+,87.0,, 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,, @@ -930,7 +930,6 @@ Function,+,date_time_input_alloc,DateTimeInput*, Function,+,date_time_input_free,void,DateTimeInput* Function,+,date_time_input_get_view,View*,DateTimeInput* Function,+,date_time_input_set_editable_fields,void,"DateTimeInput*, _Bool, _Bool, _Bool, _Bool, _Bool, _Bool" -Function,+,date_time_input_set_header_text,void,"DateTimeInput*, const char*" Function,+,date_time_input_set_result_callback,void,"DateTimeInput*, DateTimeChangedCallback, DateTimeDoneCallback, void*, DateTime*" Function,+,datetime_datetime_to_timestamp,uint32_t,DateTime* Function,+,datetime_get_days_per_month,uint8_t,"_Bool, uint8_t" From 735664da9679ba11da15934d7d7bf829c7534328 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 23 Sep 2025 15:07:06 -0700 Subject: [PATCH 3/5] Add F18 target --- targets/f18/api_symbols.csv | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/targets/f18/api_symbols.csv b/targets/f18/api_symbols.csv index 23c9edb63..165f367c6 100644 --- a/targets/f18/api_symbols.csv +++ b/targets/f18/api_symbols.csv @@ -1,5 +1,5 @@ entry,status,name,type,params -Version,+,86.0,, +Version,v,86.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/date_time_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,, @@ -833,6 +834,11 @@ Function,+,crc32_calc_buffer,uint32_t,"uint32_t, const void*, size_t" Function,+,crc32_calc_file,uint32_t,"File*, const FileCrcProgressCb, void*" Function,-,ctermid,char*,char* Function,-,cuserid,char*,char* +Function,+,date_time_input_alloc,DateTimeInput*, +Function,+,date_time_input_free,void,DateTimeInput* +Function,+,date_time_input_get_view,View*,DateTimeInput* +Function,+,date_time_input_set_editable_fields,void,"DateTimeInput*, _Bool, _Bool, _Bool, _Bool, _Bool, _Bool" +Function,+,date_time_input_set_result_callback,void,"DateTimeInput*, DateTimeChangedCallback, DateTimeDoneCallback, void*, DateTime*" Function,+,datetime_datetime_to_timestamp,uint32_t,DateTime* Function,+,datetime_get_days_per_month,uint8_t,"_Bool, uint8_t" Function,+,datetime_get_days_per_year,uint16_t,uint16_t From d35976cf6cf27c2b5a04db873c3a1c7c2069cba8 Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 23 Sep 2025 15:13:25 -0700 Subject: [PATCH 4/5] Add 12-hour time support --- ...ple_date_time_input_scene_show_date_time.c | 21 ++++-- .../services/gui/modules/date_time_input.c | 67 ++++++++++++++++++- 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c index 666db14a6..15e4cce08 100644 --- a/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c +++ b/applications/examples/example_date_time_input/scenes/example_date_time_input_scene_show_date_time.c @@ -13,17 +13,30 @@ static void example_date_time_input_scene_update_view(void* context) { dialog_ex_set_header(dialog_ex, "The date and time are", 64, 0, AlignCenter, AlignTop); - char buffer[26] = {}; + uint8_t hour = app->date_time.hour; + char label_hour[4] = ""; + if(furi_hal_rtc_get_locale_timeformat() == FuriHalRtcLocaleTimeFormat12h) { + if(hour < 12) { + snprintf(label_hour, sizeof(label_hour), " AM"); + } else { + snprintf(label_hour, sizeof(label_hour), " PM"); + } + hour %= 12; + if(hour == 0) hour = 12; + } + + char buffer[29] = {}; snprintf( buffer, sizeof(buffer), - "%04d-%02d-%02d\n%02d:%02d:%02d", + "%04d-%02d-%02d\n%02d:%02d:%02d%s", app->date_time.year, app->date_time.month, app->date_time.day, - app->date_time.hour, + hour, app->date_time.minute, - app->date_time.second); + app->date_time.second, + label_hour); dialog_ex_set_text(dialog_ex, buffer, 64, 29, AlignCenter, AlignCenter); dialog_ex_set_left_button_text(dialog_ex, "Date"); diff --git a/applications/services/gui/modules/date_time_input.c b/applications/services/gui/modules/date_time_input.c index eb703cda2..a80634823 100644 --- a/applications/services/gui/modules/date_time_input.c +++ b/applications/services/gui/modules/date_time_input.c @@ -1,4 +1,5 @@ #include "date_time_input.h" +#include "furi_hal_rtc.h" #include #include @@ -88,9 +89,31 @@ static inline void date_time_input_draw_block( } } -static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputModel* model) { - furi_check(model->datetime); +static inline void date_time_input_draw_text( + Canvas* canvas, + int32_t x, + int32_t y, + size_t w, + size_t h, + Font font, + EditState state, + const char* text) { + furi_assert(canvas); + furi_assert(text); + canvas_set_color(canvas, ColorBlack); + if(state != EditStateDisabled && state != EditStateNone) { + canvas_set_color(canvas, ColorWhite); + } + + canvas_set_font(canvas, font); + canvas_draw_str_aligned(canvas, x + w / 2, y + h / 2, AlignCenter, AlignCenter, text); + if(state != EditStateNone) { + canvas_set_color(canvas, ColorBlack); + } +} + +static void date_time_input_draw_hour_24hr_callback(Canvas* canvas, DateTimeInputModel* model) { char buffer[64]; canvas_set_font(canvas, FontSecondary); @@ -102,6 +125,46 @@ static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputMode canvas, 30, ROW_1_Y, 28, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); +} + +static void date_time_input_draw_hour_12hr_callback(Canvas* canvas, DateTimeInputModel* model) { + char buffer[64]; + + canvas_set_font(canvas, FontSecondary); + canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S"); + canvas_set_font(canvas, FontPrimary); + + uint8_t hour = model->datetime->hour % 12; + // Show 12:00 instead of 00:00 for 12-hour time + if(hour == 0) hour = 12; + + // Placeholder XX since FontBigNumbers can't draw letters + snprintf(buffer, sizeof(buffer), "%02u XX", hour); + date_time_input_draw_block( + canvas, 2, ROW_1_Y, 56, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); + canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2); + canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); + + if(model->datetime->hour < 12) { + snprintf(buffer, sizeof(buffer), "AM"); + } else { + snprintf(buffer, sizeof(buffer), "PM"); + } + date_time_input_draw_text( + canvas, 28, ROW_1_Y + 3, 28, ROW_1_H, FontPrimary, get_state(model, 1, 0, hour), buffer); +} + +static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputModel* model) { + furi_check(model->datetime); + + char buffer[64]; + + // Draw hour depending on RTC time format + if(furi_hal_rtc_get_locale_timeformat() == FuriHalRtcLocaleTimeFormat24h) { + date_time_input_draw_hour_24hr_callback(canvas, model); + } else { + date_time_input_draw_hour_12hr_callback(canvas, model); + } snprintf(buffer, sizeof(buffer), "%02u", model->datetime->minute); date_time_input_draw_block( From 2dd3673dcd0d4ba9124b79bf1dd9ca4d4428d97f Mon Sep 17 00:00:00 2001 From: "Aaron Tulino (Aaronjamt)" Date: Tue, 23 Sep 2025 15:33:28 -0700 Subject: [PATCH 5/5] Improve 12-hour UI --- .../services/gui/modules/date_time_input.c | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/applications/services/gui/modules/date_time_input.c b/applications/services/gui/modules/date_time_input.c index a80634823..be64ed165 100644 --- a/applications/services/gui/modules/date_time_input.c +++ b/applications/services/gui/modules/date_time_input.c @@ -114,7 +114,7 @@ static inline void date_time_input_draw_text( } static void date_time_input_draw_hour_24hr_callback(Canvas* canvas, DateTimeInputModel* model) { - char buffer[64]; + char buffer[4]; canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S"); @@ -128,36 +128,40 @@ static void date_time_input_draw_hour_24hr_callback(Canvas* canvas, DateTimeInpu } static void date_time_input_draw_hour_12hr_callback(Canvas* canvas, DateTimeInputModel* model) { - char buffer[64]; + char buffer[4]; canvas_set_font(canvas, FontSecondary); - canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S"); + canvas_draw_str(canvas, 0, ROW_1_Y - 2, " H H M M S S"); canvas_set_font(canvas, FontPrimary); uint8_t hour = model->datetime->hour % 12; // Show 12:00 instead of 00:00 for 12-hour time if(hour == 0) hour = 12; - // Placeholder XX since FontBigNumbers can't draw letters - snprintf(buffer, sizeof(buffer), "%02u XX", hour); + // Placeholder spaces to make room for AM/PM since FontBigNumbers can't draw letters date_time_input_draw_block( - canvas, 2, ROW_1_Y, 56, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); + canvas, 8, ROW_1_Y, 50, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7, 2, 2); canvas_draw_box(canvas, 60, ROW_1_Y + ROW_1_H - 7 - 6, 2, 2); - if(model->datetime->hour < 12) { - snprintf(buffer, sizeof(buffer), "AM"); - } else { - snprintf(buffer, sizeof(buffer), "PM"); - } + snprintf(buffer, sizeof(buffer), "%02u", hour); date_time_input_draw_text( - canvas, 28, ROW_1_Y + 3, 28, ROW_1_H, FontPrimary, get_state(model, 1, 0, hour), buffer); + canvas, 8, ROW_1_Y, 30, ROW_1_H, FontBigNumbers, get_state(model, 1, 0, hour), buffer); + + // The AM and PM text shift by 1 pixel so compensate to make them line up + if(model->datetime->hour < 12) { + date_time_input_draw_text( + canvas, 30, ROW_1_Y + 3, 30, ROW_1_H, FontPrimary, get_state(model, 1, 0, hour), "AM"); + } else { + date_time_input_draw_text( + canvas, 31, ROW_1_Y + 3, 30, ROW_1_H, FontPrimary, get_state(model, 1, 0, hour), "PM"); + } } static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputModel* model) { furi_check(model->datetime); - char buffer[64]; + char buffer[4]; // Draw hour depending on RTC time format if(furi_hal_rtc_get_locale_timeformat() == FuriHalRtcLocaleTimeFormat24h) { @@ -180,7 +184,7 @@ static void date_time_input_draw_time_callback(Canvas* canvas, DateTimeInputMode static void date_time_input_draw_date_callback(Canvas* canvas, DateTimeInputModel* model) { furi_check(model->datetime); - char buffer[64]; + char buffer[6]; canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 0, ROW_0_Y - 2, " Y Y Y Y M M D D");