diff --git a/CODING_STYLE.md b/CODING_STYLE.md index c62009eff..002c67f24 100644 --- a/CODING_STYLE.md +++ b/CODING_STYLE.md @@ -48,7 +48,7 @@ Almost everything in flipper firmware is built around this concept. # C coding style - Tab is 4 spaces -- Use `fbt format` to reformat source code and check style guide before commit +- Use `./fbt format` to reformat source code and check style guide before commit ## Naming diff --git a/applications/debug/unit_tests/dialogs/dialogs_file_browser_options.c b/applications/debug/unit_tests/dialogs/dialogs_file_browser_options.c new file mode 100644 index 000000000..2d5bad4c8 --- /dev/null +++ b/applications/debug/unit_tests/dialogs/dialogs_file_browser_options.c @@ -0,0 +1,32 @@ +#include + +#include "../minunit.h" + +MU_TEST(test_dialog_file_browser_set_basic_options_should_init_all_fields) { + mu_assert( + sizeof(DialogsFileBrowserOptions) == 28, + "Changes to `DialogsFileBrowserOptions` should also be reflected in `dialog_file_browser_set_basic_options`"); + + DialogsFileBrowserOptions options; + dialog_file_browser_set_basic_options(&options, ".fap", NULL); + // note: this assertions can safely be changed, their primary purpose is to remind the maintainer + // to update `dialog_file_browser_set_basic_options` by including all structure fields in it + mu_assert_string_eq(".fap", options.extension); + mu_assert_null(options.base_path); + mu_assert(options.skip_assets, "`skip_assets` should default to `true"); + mu_assert(options.hide_dot_files, "`hide_dot_files` should default to `true"); + mu_assert_null(options.icon); + mu_assert(options.hide_ext, "`hide_ext` should default to `true"); + mu_assert_null(options.item_loader_callback); + mu_assert_null(options.item_loader_context); +} + +MU_TEST_SUITE(dialogs_file_browser_options) { + MU_RUN_TEST(test_dialog_file_browser_set_basic_options_should_init_all_fields); +} + +int run_minunit_test_dialogs_file_browser_options() { + MU_RUN_SUITE(dialogs_file_browser_options); + + return MU_EXIT_CODE; +} diff --git a/applications/debug/unit_tests/test_index.c b/applications/debug/unit_tests/test_index.c index ac71ca397..9d7631bfe 100644 --- a/applications/debug/unit_tests/test_index.c +++ b/applications/debug/unit_tests/test_index.c @@ -27,6 +27,7 @@ int run_minunit_test_nfc(); int run_minunit_test_bit_lib(); int run_minunit_test_float_tools(); int run_minunit_test_bt(); +int run_minunit_test_dialogs_file_browser_options(); typedef int (*UnitTestEntry)(); @@ -55,6 +56,8 @@ const UnitTest unit_tests[] = { {.name = "bit_lib", .entry = run_minunit_test_bit_lib}, {.name = "float_tools", .entry = run_minunit_test_float_tools}, {.name = "bt", .entry = run_minunit_test_bt}, + {.name = "dialogs_file_browser_options", + .entry = run_minunit_test_dialogs_file_browser_options}, }; void minunit_print_progress() { diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index af60e89b0..4d330532e 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -418,9 +418,22 @@ static bool archive_view_input(InputEvent* event, void* context) { browser->view, ArchiveBrowserViewModel * model, { + int32_t scroll_speed = 1; + if(model->button_held_for_ticks > 5) { + if(model->button_held_for_ticks % 2) { + scroll_speed = 0; + } else { + scroll_speed = model->button_held_for_ticks > 9 ? 4 : 2; + } + } + if(event->key == InputKeyUp) { + if(model->item_idx < scroll_speed) { + scroll_speed = model->item_idx; + } + model->item_idx = - ((model->item_idx - 1) + model->item_cnt) % model->item_cnt; + ((model->item_idx - scroll_speed) + model->item_cnt) % model->item_cnt; if(is_file_list_load_required(model)) { model->list_loading = true; browser->callback(ArchiveBrowserEventLoadPrevItems, browser->context); @@ -429,8 +442,14 @@ static bool archive_view_input(InputEvent* event, void* context) { browser->callback(ArchiveBrowserEventFavMoveUp, browser->context); } model->scroll_counter = 0; + model->button_held_for_ticks += 1; } else if(event->key == InputKeyDown) { - model->item_idx = (model->item_idx + 1) % model->item_cnt; + int32_t count = model->item_cnt; + if(model->item_idx >= (count - scroll_speed)) { + scroll_speed = model->item_cnt - model->item_idx - 1; + } + + model->item_idx = (model->item_idx + scroll_speed) % model->item_cnt; if(is_file_list_load_required(model)) { model->list_loading = true; browser->callback(ArchiveBrowserEventLoadNextItems, browser->context); @@ -439,6 +458,7 @@ static bool archive_view_input(InputEvent* event, void* context) { browser->callback(ArchiveBrowserEventFavMoveDown, browser->context); } model->scroll_counter = 0; + model->button_held_for_ticks += 1; } }, false); @@ -477,6 +497,14 @@ static bool archive_view_input(InputEvent* event, void* context) { } } + if(event->type == InputTypeRelease) { + with_view_model( + browser->view, + ArchiveBrowserViewModel * model, + { model->button_held_for_ticks = 0; }, + true); + } + return true; } diff --git a/applications/main/archive/views/archive_browser_view.h b/applications/main/archive/views/archive_browser_view.h index 885df4d09..47e820599 100644 --- a/applications/main/archive/views/archive_browser_view.h +++ b/applications/main/archive/views/archive_browser_view.h @@ -110,6 +110,8 @@ typedef struct { int32_t array_offset; int32_t list_offset; size_t scroll_counter; + + uint32_t button_held_for_ticks; } ArchiveBrowserViewModel; void archive_browser_set_callback( diff --git a/applications/services/dialogs/dialogs.h b/applications/services/dialogs/dialogs.h index 4c1b675a6..39b15c67c 100644 --- a/applications/services/dialogs/dialogs.h +++ b/applications/services/dialogs/dialogs.h @@ -16,7 +16,8 @@ typedef struct DialogsApp DialogsApp; /****************** FILE BROWSER ******************/ /** - * File browser dialog extra options + * File browser dialog extra options. + * This can be default-initialized using {@link dialog_file_browser_set_basic_options}. * @param extension file extension to be offered for selection * @param base_path root folder path for navigation with back key * @param skip_assets true - do not show assets folders @@ -38,8 +39,10 @@ typedef struct { } DialogsFileBrowserOptions; /** - * Initialize file browser dialog options - * and set default values + * Initialize file browser dialog options and set default values. + * This is guaranteed to initialize all fields + * so it is safe to pass pointer to uninitialized {@code options} + * and assume that the data behind it becomes fully initialized after the call. * @param options pointer to options structure * @param extension file extension to filter * @param icon file icon pointer, NULL for default icon diff --git a/applications/services/gui/modules/file_browser.c b/applications/services/gui/modules/file_browser.c index 4f11ffe8d..4e6a96183 100644 --- a/applications/services/gui/modules/file_browser.c +++ b/applications/services/gui/modules/file_browser.c @@ -147,6 +147,8 @@ typedef struct { const Icon* file_icon; bool hide_ext; size_t scroll_counter; + + uint32_t button_held_for_ticks; } FileBrowserModel; static const Icon* BrowserItemIcons[] = { @@ -661,9 +663,22 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) { browser->view, FileBrowserModel * model, { + int32_t scroll_speed = 1; + if(model->button_held_for_ticks > 5) { + if(model->button_held_for_ticks % 2) { + scroll_speed = 0; + } else { + scroll_speed = model->button_held_for_ticks > 9 ? 5 : 3; + } + } + if(event->key == InputKeyUp) { + if(model->item_idx < scroll_speed) { + scroll_speed = model->item_idx; + } + model->item_idx = - ((model->item_idx - 1) + model->item_cnt) % model->item_cnt; + ((model->item_idx - scroll_speed) + model->item_cnt) % model->item_cnt; if(browser_is_list_load_required(model)) { model->list_loading = true; int32_t load_offset = CLAMP( @@ -674,8 +689,15 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) { browser->worker, load_offset, ITEM_LIST_LEN_MAX); } model->scroll_counter = 0; + + model->button_held_for_ticks += 1; } else if(event->key == InputKeyDown) { - model->item_idx = (model->item_idx + 1) % model->item_cnt; + int32_t count = model->item_cnt; + if(model->item_idx + scroll_speed >= count) { + scroll_speed = count - model->item_idx - 1; + } + + model->item_idx = (model->item_idx + scroll_speed) % model->item_cnt; if(browser_is_list_load_required(model)) { model->list_loading = true; int32_t load_offset = CLAMP( @@ -686,11 +708,19 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) { browser->worker, load_offset, ITEM_LIST_LEN_MAX); } model->scroll_counter = 0; + + model->button_held_for_ticks += 1; } }, false); browser_update_offset(browser); consumed = true; + } else if(event->type == InputTypeRelease) { + with_view_model( + browser->view, + FileBrowserModel * model, + { model->button_held_for_ticks = 0; }, + true); } } else if(event->key == InputKeyOk) { if(event->type == InputTypeShort) { diff --git a/applications/services/gui/modules/text_box.c b/applications/services/gui/modules/text_box.c index 6dc558815..77d30b8eb 100644 --- a/applications/services/gui/modules/text_box.c +++ b/applications/services/gui/modules/text_box.c @@ -6,6 +6,8 @@ struct TextBox { View* view; + + uint16_t button_held_for_ticks; }; typedef struct { @@ -19,36 +21,52 @@ typedef struct { bool formatted; } TextBoxModel; -static void text_box_process_down(TextBox* text_box) { +static void text_box_process_down(TextBox* text_box, uint8_t lines) { with_view_model( text_box->view, TextBoxModel * model, { - if(model->scroll_pos < model->scroll_num - 1) { - model->scroll_pos++; - // Search next line start - while(*model->text_pos++ != '\n') - ; + if(model->scroll_pos < model->scroll_num - lines) { + model->scroll_pos += lines; + for(uint8_t i = 0; i < lines; i++) { + // Search next line start + while(*model->text_pos++ != '\n') + ; + } + } else if(lines > 1) { + lines = model->scroll_num - model->scroll_pos - 1; + model->scroll_pos = model->scroll_num - 1; + for(uint8_t i = 0; i < lines; i++) { + // Search next line start + while(*model->text_pos++ != '\n') + ; + } } }, true); } -static void text_box_process_up(TextBox* text_box) { +static void text_box_process_up(TextBox* text_box, uint8_t lines) { with_view_model( text_box->view, TextBoxModel * model, { - if(model->scroll_pos > 0) { - model->scroll_pos--; - // Reach last symbol of previous line - model->text_pos--; - // Search previous line start - while((model->text_pos != model->text) && (*(--model->text_pos) != '\n')) - ; - if(*model->text_pos == '\n') { - model->text_pos++; + if(model->scroll_pos > lines - 1) { + model->scroll_pos -= lines; + for(uint8_t i = 0; i < lines; i++) { + // Reach last symbol of previous line + model->text_pos--; + // Search previous line start + while((model->text_pos != model->text) && (*(--model->text_pos) != '\n')) + ; + if(*model->text_pos == '\n') { + model->text_pos++; + } } + } else if(lines > 1) { + lines = model->scroll_pos; + model->scroll_pos = 0; + model->text_pos = (char*)model->text; } }, true); @@ -120,14 +138,28 @@ static bool text_box_view_input_callback(InputEvent* event, void* context) { TextBox* text_box = context; bool consumed = false; - if(event->type == InputTypeShort) { + if(event->type == InputTypeShort || event->type == InputTypeRepeat) { + int32_t scroll_speed = 1; + if(text_box->button_held_for_ticks > 5) { + if(text_box->button_held_for_ticks % 2) { + scroll_speed = 0; + } else { + scroll_speed = text_box->button_held_for_ticks > 9 ? 5 : 3; + } + } + if(event->key == InputKeyDown) { - text_box_process_down(text_box); + text_box_process_down(text_box, scroll_speed); consumed = true; } else if(event->key == InputKeyUp) { - text_box_process_up(text_box); + text_box_process_up(text_box, scroll_speed); consumed = true; } + + text_box->button_held_for_ticks++; + } else if(event->type == InputTypeRelease) { + text_box->button_held_for_ticks = 0; + consumed = true; } return consumed; }