From 168fa72d53bd83a77ead8cc4db04ba403f0299c2 Mon Sep 17 00:00:00 2001 From: Astra <93453568+Astrrra@users.noreply.github.com> Date: Tue, 27 Jun 2023 12:22:35 +0300 Subject: [PATCH 1/3] [FL-3373] Scroll acceleration (#2784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Support for scroll acceleration * Revert keyboard acceleration * Add scroll acceleration to the text box * Remove redundant code from the file manager input handler * Archive: slightly better scrolling * Gui,Archive: final version of accelerated scrolling Co-authored-by: あく --- .../main/archive/views/archive_browser_view.c | 32 ++++++++- .../main/archive/views/archive_browser_view.h | 2 + .../services/gui/modules/file_browser.c | 34 ++++++++- applications/services/gui/modules/text_box.c | 70 ++++++++++++++----- 4 files changed, 115 insertions(+), 23 deletions(-) diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index 2aca3c02b..7e2f84fc2 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -334,9 +334,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); @@ -345,8 +358,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); @@ -355,6 +374,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; } }, true); @@ -391,6 +411,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 0a000e5ac..25490aedd 100644 --- a/applications/main/archive/views/archive_browser_view.h +++ b/applications/main/archive/views/archive_browser_view.h @@ -100,6 +100,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/gui/modules/file_browser.c b/applications/services/gui/modules/file_browser.c index d12a00ba5..b2a2c3c23 100644 --- a/applications/services/gui/modules/file_browser.c +++ b/applications/services/gui/modules/file_browser.c @@ -118,6 +118,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[] = { @@ -589,9 +591,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( @@ -602,8 +617,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( @@ -614,11 +636,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; } }, true); 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 01ccdbf52..0e4aae944 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; } From 75354ec5bac8be5f631213c8e236123c2bcea075 Mon Sep 17 00:00:00 2001 From: Petr Portnov | PROgrm_JARvis Date: Tue, 27 Jun 2023 12:46:04 +0300 Subject: [PATCH 2/3] fix: make `dialog_file_browser_set_basic_options` initialize all fields (#2756) * fix: make `dialog_file_browser_set_basic_options` initialize all fields * fix(GH-2756): use alternative test for `test_dialog_file_browser_set_basic_options_should_init_all_fields` Co-authored-by: Aleksandr Kutuzov --- CODING_STYLE.md | 2 +- .../dialogs/dialogs_file_browser_options.c | 32 +++++++++++++++++++ applications/debug/unit_tests/test_index.c | 3 ++ applications/services/dialogs/dialogs.c | 3 +- applications/services/dialogs/dialogs.h | 9 ++++-- 5 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 applications/debug/unit_tests/dialogs/dialogs_file_browser_options.c 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/services/dialogs/dialogs.c b/applications/services/dialogs/dialogs.c index 3908ca31b..10c08a991 100644 --- a/applications/services/dialogs/dialogs.c +++ b/applications/services/dialogs/dialogs.c @@ -9,12 +9,13 @@ void dialog_file_browser_set_basic_options( const char* extension, const Icon* icon) { options->extension = extension; + options->base_path = NULL; options->skip_assets = true; + options->hide_dot_files = true; options->icon = icon; options->hide_ext = true; options->item_loader_callback = NULL; options->item_loader_context = NULL; - options->base_path = NULL; } static DialogsApp* dialogs_app_alloc() { 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 From 0a5508a8a1ab202131262702901eaf0e388b212a Mon Sep 17 00:00:00 2001 From: PpHd Date: Tue, 27 Jun 2023 12:50:09 +0200 Subject: [PATCH 3/3] Fix M*LIB usage (#2762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix M*LIB usage * Fix oplist definition of SubGhzFrequencyAnalyzerLogItem * Fix oplist definition of M_CSTR_DUP_OPLIST * Remove dependency of furi_string_utf8_decode to the internal definition of string_unicode_t * Replace obsolete macro M_IF_DEFAULT1 to M_DEFAULT_ARGS Co-authored-by: hedger Co-authored-by: あく --- ...subghz_frequency_analyzer_log_item_array.h | 7 ++- furi/core/string.c | 4 +- furi/core/string.h | 44 ++++++++----------- lib/toolbox/m_cstr_dup.h | 19 ++++---- 4 files changed, 37 insertions(+), 37 deletions(-) diff --git a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h index b94ebe380..12c6bef6a 100644 --- a/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h +++ b/applications/main/subghz/helpers/subghz_frequency_analyzer_log_item_array.h @@ -27,7 +27,12 @@ TUPLE_DEF2( (rssi_max, uint8_t)) /* Register globally the oplist */ #define M_OPL_SubGhzFrequencyAnalyzerLogItem_t() \ - TUPLE_OPLIST(SubGhzFrequencyAnalyzerLogItem, M_POD_OPLIST, M_DEFAULT_OPLIST, M_DEFAULT_OPLIST) + TUPLE_OPLIST( \ + SubGhzFrequencyAnalyzerLogItem, \ + M_DEFAULT_OPLIST, \ + M_DEFAULT_OPLIST, \ + M_DEFAULT_OPLIST, \ + M_DEFAULT_OPLIST) /* Define the array, register the oplist and define further algorithms on it */ ARRAY_DEF(SubGhzFrequencyAnalyzerLogItemArray, SubGhzFrequencyAnalyzerLogItem_t) diff --git a/furi/core/string.c b/furi/core/string.c index 4384fe06a..682c8d409 100644 --- a/furi/core/string.c +++ b/furi/core/string.c @@ -296,7 +296,9 @@ static FuriStringUTF8State state_to_furi_state(m_str1ng_utf8_state_e state) { } void furi_string_utf8_decode(char c, FuriStringUTF8State* state, FuriStringUnicodeValue* unicode) { + string_unicode_t m_u = *unicode; m_str1ng_utf8_state_e m_state = furi_state_to_state(*state); - m_str1ng_utf8_decode(c, &m_state, unicode); + m_str1ng_utf8_decode(c, &m_state, &m_u); *state = state_to_furi_state(m_state); + *unicode = m_u; } diff --git a/furi/core/string.h b/furi/core/string.h index 0523d3ba0..7529deacd 100644 --- a/furi/core/string.h +++ b/furi/core/string.h @@ -633,20 +633,17 @@ void furi_string_utf8_decode(char c, FuriStringUTF8State* state, FuriStringUnico * @brief Search for a string (or C string) in a string * (string, [c]string[, start=0]) */ -#define furi_string_search(v, ...) \ - M_APPLY( \ - FURI_STRING_SELECT3, \ - furi_string_search, \ - furi_string_search_str, \ - v, \ - M_IF_DEFAULT1(0, __VA_ARGS__)) - +#define furi_string_search(...) \ + M_APPLY( \ + FURI_STRING_SELECT3, \ + furi_string_search, \ + furi_string_search_str, \ + M_DEFAULT_ARGS(3, (0), __VA_ARGS__)) /** * @brief Search for a C string in a string * (string, cstring[, start=0]) */ -#define furi_string_search_str(v, ...) \ - M_APPLY(furi_string_search_str, v, M_IF_DEFAULT1(0, __VA_ARGS__)) +#define furi_string_search_str(...) furi_string_search_str(M_DEFAULT_ARGS(3, (0), __VA_ARGS__)) /** * @brief Test if the string starts with the given string (or C string). @@ -672,41 +669,36 @@ void furi_string_utf8_decode(char c, FuriStringUTF8State* state, FuriStringUnico * @brief Trim a string from the given set of characters (default is " \n\r\t"). * (string[, set=" \n\r\t"]) */ -#define furi_string_trim(...) M_APPLY(furi_string_trim, M_IF_DEFAULT1(" \n\r\t", __VA_ARGS__)) +#define furi_string_trim(...) furi_string_trim(M_DEFAULT_ARGS(2, (" \n\r\t"), __VA_ARGS__)) /** * @brief Search for a character in a string. * (string, character[, start=0]) */ -#define furi_string_search_char(v, ...) \ - M_APPLY(furi_string_search_char, v, M_IF_DEFAULT1(0, __VA_ARGS__)) +#define furi_string_search_char(...) furi_string_search_char(M_DEFAULT_ARGS(3, (0), __VA_ARGS__)) /** * @brief Reverse Search for a character in a string. * (string, character[, start=0]) */ -#define furi_string_search_rchar(v, ...) \ - M_APPLY(furi_string_search_rchar, v, M_IF_DEFAULT1(0, __VA_ARGS__)) +#define furi_string_search_rchar(...) furi_string_search_rchar(M_DEFAULT_ARGS(3, (0), __VA_ARGS__)) /** * @brief Replace a string to another string (or C string to another C string) in a string. * (string, [c]string, [c]string[, start=0]) */ -#define furi_string_replace(a, b, ...) \ - M_APPLY( \ - FURI_STRING_SELECT4, \ - furi_string_replace, \ - furi_string_replace_str, \ - a, \ - b, \ - M_IF_DEFAULT1(0, __VA_ARGS__)) +#define furi_string_replace(...) \ + M_APPLY( \ + FURI_STRING_SELECT4, \ + furi_string_replace, \ + furi_string_replace_str, \ + M_DEFAULT_ARGS(4, (0), __VA_ARGS__)) /** * @brief Replace a C string to another C string in a string. * (string, cstring, cstring[, start=0]) */ -#define furi_string_replace_str(a, b, ...) \ - M_APPLY(furi_string_replace_str, a, b, M_IF_DEFAULT1(0, __VA_ARGS__)) +#define furi_string_replace_str(...) furi_string_replace_str(M_DEFAULT_ARGS(4, (0), __VA_ARGS__)) /** * @brief INIT OPLIST for FuriString. @@ -743,4 +735,4 @@ void furi_string_utf8_decode(char c, FuriStringUTF8State* state, FuriStringUnico #ifdef __cplusplus } -#endif \ No newline at end of file +#endif diff --git a/lib/toolbox/m_cstr_dup.h b/lib/toolbox/m_cstr_dup.h index 0555f72c6..11b7fe35a 100644 --- a/lib/toolbox/m_cstr_dup.h +++ b/lib/toolbox/m_cstr_dup.h @@ -2,15 +2,16 @@ #include #define M_INIT_DUP(a) ((a) = strdup("")) -#define M_SET_DUP(a, b) (M_CHECK_DEFAULT_TYPE(a), free((void*)a), (a) = strdup(b)) +#define M_INIT_SET_DUP(a, b) ((a) = strdup(b)) +#define M_SET_DUP(a, b) (free((void*)a), (a) = strdup(b)) #define M_CLEAR_DUP(a) (free((void*)a)) -#define M_CSTR_DUP_OPLIST \ - (INIT(M_INIT_DUP), \ - INIT_SET(M_SET_DUP), \ - SET(M_SET_DUP), \ - CLEAR(M_CLEAR_DUP), \ - HASH(m_core_cstr_hash), \ - EQUAL(M_CSTR_EQUAL), \ - CMP(strcmp), \ +#define M_CSTR_DUP_OPLIST \ + (INIT(M_INIT_DUP), \ + INIT_SET(M_INIT_SET_DUP), \ + SET(M_SET_DUP), \ + CLEAR(M_CLEAR_DUP), \ + HASH(m_core_cstr_hash), \ + EQUAL(M_CSTR_EQUAL), \ + CMP(strcmp), \ TYPE(const char*))