From e7107e39f76570aa25de7be7c8584eb1cbe6fff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=81=8F?= Date: Tue, 20 Dec 2022 20:11:52 +0900 Subject: [PATCH] Gui: scrollable long file names in FileBrowser and Archive Browser (#2159) * Gui: scrollable long file names in FileBrowser * Archive: scroll long file names * Gui: elements code cleanup --- .../main/archive/views/archive_browser_view.c | 52 ++++++++++++++++-- .../main/archive/views/archive_browser_view.h | 2 + applications/services/dialogs/view_holder.c | 8 +++ applications/services/gui/elements.c | 46 ++++++++++++++++ applications/services/gui/elements.h | 19 +++++++ .../services/gui/modules/file_browser.c | 55 +++++++++++++++++-- firmware/targets/f7/api_symbols.csv | 1 + 7 files changed, 174 insertions(+), 9 deletions(-) diff --git a/applications/main/archive/views/archive_browser_view.c b/applications/main/archive/views/archive_browser_view.c index 65be4213..2aca3c02 100644 --- a/applications/main/archive/views/archive_browser_view.c +++ b/applications/main/archive/views/archive_browser_view.c @@ -5,6 +5,9 @@ #include "archive_browser_view.h" #include "../helpers/archive_browser.h" +#define SCROLL_INTERVAL (333) +#define SCROLL_DELAY (2) + static const char* ArchiveTabNames[] = { [ArchiveTabFavorites] = "Favorites", [ArchiveTabIButton] = "iButton", @@ -146,13 +149,18 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) { furi_string_set(str_buf, "---"); } - elements_string_fit_width( - canvas, str_buf, (scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset); + size_t scroll_counter = model->scroll_counter; if(model->item_idx == idx) { archive_draw_frame(canvas, i, scrollbar, model->move_fav); + if(scroll_counter < SCROLL_DELAY) { + scroll_counter = 0; + } else { + scroll_counter -= SCROLL_DELAY; + } } else { canvas_set_color(canvas, ColorBlack); + scroll_counter = 0; } if(custom_icon_data) { @@ -162,8 +170,15 @@ static void draw_list(Canvas* canvas, ArchiveBrowserViewModel* model) { canvas_draw_icon( canvas, 2 + x_offset, 16 + i * FRAME_HEIGHT, ArchiveItemIcons[file_type]); } - canvas_draw_str( - canvas, 15 + x_offset, 24 + i * FRAME_HEIGHT, furi_string_get_cstr(str_buf)); + + elements_scrollable_text_line( + canvas, + 15 + x_offset, + 24 + i * FRAME_HEIGHT, + ((scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX) - x_offset), + str_buf, + scroll_counter, + (model->item_idx != idx)); furi_string_free(str_buf); } @@ -329,6 +344,7 @@ static bool archive_view_input(InputEvent* event, void* context) { if(move_fav_mode) { browser->callback(ArchiveBrowserEventFavMoveUp, browser->context); } + model->scroll_counter = 0; } else if(event->key == InputKeyDown) { model->item_idx = (model->item_idx + 1) % model->item_cnt; if(is_file_list_load_required(model)) { @@ -338,6 +354,7 @@ static bool archive_view_input(InputEvent* event, void* context) { if(move_fav_mode) { browser->callback(ArchiveBrowserEventFavMoveDown, browser->context); } + model->scroll_counter = 0; } }, true); @@ -377,6 +394,27 @@ static bool archive_view_input(InputEvent* event, void* context) { return true; } +static void browser_scroll_timer(void* context) { + furi_assert(context); + ArchiveBrowserView* browser = context; + with_view_model( + browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter++; }, true); +} + +static void browser_view_enter(void* context) { + furi_assert(context); + ArchiveBrowserView* browser = context; + with_view_model( + browser->view, ArchiveBrowserViewModel * model, { model->scroll_counter = 0; }, true); + furi_timer_start(browser->scroll_timer, SCROLL_INTERVAL); +} + +static void browser_view_exit(void* context) { + furi_assert(context); + ArchiveBrowserView* browser = context; + furi_timer_stop(browser->scroll_timer); +} + ArchiveBrowserView* browser_alloc() { ArchiveBrowserView* browser = malloc(sizeof(ArchiveBrowserView)); browser->view = view_alloc(); @@ -384,6 +422,10 @@ ArchiveBrowserView* browser_alloc() { view_set_context(browser->view, browser); view_set_draw_callback(browser->view, archive_view_render); view_set_input_callback(browser->view, archive_view_input); + view_set_enter_callback(browser->view, browser_view_enter); + view_set_exit_callback(browser->view, browser_view_exit); + + browser->scroll_timer = furi_timer_alloc(browser_scroll_timer, FuriTimerTypePeriodic, browser); browser->path = furi_string_alloc_set(archive_get_default_path(TAB_DEFAULT)); @@ -402,6 +444,8 @@ ArchiveBrowserView* browser_alloc() { void browser_free(ArchiveBrowserView* browser) { furi_assert(browser); + furi_timer_free(browser->scroll_timer); + if(browser->worker_running) { file_browser_worker_free(browser->worker); } diff --git a/applications/main/archive/views/archive_browser_view.h b/applications/main/archive/views/archive_browser_view.h index 308af4e4..915b5307 100644 --- a/applications/main/archive/views/archive_browser_view.h +++ b/applications/main/archive/views/archive_browser_view.h @@ -81,6 +81,7 @@ struct ArchiveBrowserView { FuriString* path; InputKey last_tab_switch_dir; bool is_root; + FuriTimer* scroll_timer; }; typedef struct { @@ -97,6 +98,7 @@ typedef struct { int32_t item_idx; int32_t array_offset; int32_t list_offset; + size_t scroll_counter; } ArchiveBrowserViewModel; void archive_browser_set_callback( diff --git a/applications/services/dialogs/view_holder.c b/applications/services/dialogs/view_holder.c index d2ae7750..7ab0a8e1 100644 --- a/applications/services/dialogs/view_holder.c +++ b/applications/services/dialogs/view_holder.c @@ -50,6 +50,10 @@ void view_holder_free(ViewHolder* view_holder) { void view_holder_set_view(ViewHolder* view_holder, View* view) { furi_assert(view_holder); if(view_holder->view) { + if(view_holder->view->exit_callback) { + view_holder->view->exit_callback(view_holder->view->context); + } + view_set_update_callback(view_holder->view, NULL); view_set_update_callback_context(view_holder->view, NULL); } @@ -59,6 +63,10 @@ void view_holder_set_view(ViewHolder* view_holder, View* view) { if(view_holder->view) { view_set_update_callback(view_holder->view, view_holder_update); view_set_update_callback_context(view_holder->view, view_holder); + + if(view_holder->view->enter_callback) { + view_holder->view->enter_callback(view_holder->view->context); + } } } diff --git a/applications/services/gui/elements.c b/applications/services/gui/elements.c index 0f7cf73f..955e1fbd 100644 --- a/applications/services/gui/elements.c +++ b/applications/services/gui/elements.c @@ -547,6 +547,52 @@ void elements_string_fit_width(Canvas* canvas, FuriString* string, uint8_t width } } +void elements_scrollable_text_line( + Canvas* canvas, + uint8_t x, + uint8_t y, + uint8_t width, + FuriString* string, + size_t scroll, + bool ellipsis) { + FuriString* line = furi_string_alloc_set(string); + + size_t len_px = canvas_string_width(canvas, furi_string_get_cstr(line)); + if(len_px > width) { + if(ellipsis) { + width -= canvas_string_width(canvas, "..."); + } + + // Calculate scroll size + size_t scroll_size = furi_string_size(string); + size_t right_width = 0; + for(size_t i = scroll_size; i > 0; i--) { + right_width += canvas_glyph_width(canvas, furi_string_get_char(line, i)); + if(right_width > width) break; + scroll_size--; + if(!scroll_size) break; + } + // Ensure that we have something to scroll + if(scroll_size) { + scroll_size += 3; + scroll = scroll % scroll_size; + furi_string_right(line, scroll); + } + + do { + furi_string_left(line, furi_string_size(line) - 1); + len_px = canvas_string_width(canvas, furi_string_get_cstr(line)); + } while(len_px > width); + + if(ellipsis) { + furi_string_cat(line, "..."); + } + } + + canvas_draw_str(canvas, x, y, furi_string_get_cstr(line)); + furi_string_free(line); +} + void elements_text_box( Canvas* canvas, uint8_t x, diff --git a/applications/services/gui/elements.h b/applications/services/gui/elements.h index 9f155402..162f0d41 100644 --- a/applications/services/gui/elements.h +++ b/applications/services/gui/elements.h @@ -192,6 +192,25 @@ void elements_bubble_str( */ void elements_string_fit_width(Canvas* canvas, FuriString* string, uint8_t width); +/** Draw scrollable text line + * + * @param canvas The canvas + * @param[in] x X coordinate + * @param[in] y Y coordinate + * @param[in] width The width + * @param string The string + * @param[in] scroll The scroll counter: 0 - no scroll, any other number - scroll. Just count up, everything else will be calculated on the inside. + * @param[in] ellipsis The ellipsis flag: true to add ellipse + */ +void elements_scrollable_text_line( + Canvas* canvas, + uint8_t x, + uint8_t y, + uint8_t width, + FuriString* string, + size_t scroll, + bool ellipsis); + /** Draw text box element * * @param canvas Canvas instance diff --git a/applications/services/gui/modules/file_browser.c b/applications/services/gui/modules/file_browser.c index a5daa91e..57e0018e 100644 --- a/applications/services/gui/modules/file_browser.c +++ b/applications/services/gui/modules/file_browser.c @@ -19,6 +19,9 @@ #define CUSTOM_ICON_MAX_SIZE 32 +#define SCROLL_INTERVAL (333) +#define SCROLL_DELAY (2) + typedef enum { BrowserItemTypeLoading, BrowserItemTypeBack, @@ -95,6 +98,7 @@ struct FileBrowser { void* item_context; FuriString* result_path; + FuriTimer* scroll_timer; }; typedef struct { @@ -110,6 +114,7 @@ typedef struct { const Icon* file_icon; bool hide_ext; + size_t scroll_counter; } FileBrowserModel; static const Icon* BrowserItemIcons[] = { @@ -129,6 +134,27 @@ static void browser_list_item_cb(void* context, FuriString* item_path, bool is_folder, bool is_last); static void browser_long_load_cb(void* context); +static void file_browser_scroll_timer_callback(void* context) { + furi_assert(context); + FileBrowser* browser = context; + with_view_model( + browser->view, FileBrowserModel * model, { model->scroll_counter++; }, true); +} + +static void file_browser_view_enter_callback(void* context) { + furi_assert(context); + FileBrowser* browser = context; + with_view_model( + browser->view, FileBrowserModel * model, { model->scroll_counter = 0; }, true); + furi_timer_start(browser->scroll_timer, SCROLL_INTERVAL); +} + +static void file_browser_view_exit_callback(void* context) { + furi_assert(context); + FileBrowser* browser = context; + furi_timer_stop(browser->scroll_timer); +} + FileBrowser* file_browser_alloc(FuriString* result_path) { furi_assert(result_path); FileBrowser* browser = malloc(sizeof(FileBrowser)); @@ -137,6 +163,11 @@ FileBrowser* file_browser_alloc(FuriString* result_path) { view_set_context(browser->view, browser); view_set_draw_callback(browser->view, file_browser_view_draw_callback); view_set_input_callback(browser->view, file_browser_view_input_callback); + view_set_enter_callback(browser->view, file_browser_view_enter_callback); + view_set_exit_callback(browser->view, file_browser_view_exit_callback); + + browser->scroll_timer = + furi_timer_alloc(file_browser_scroll_timer_callback, FuriTimerTypePeriodic, browser); browser->result_path = result_path; @@ -149,6 +180,8 @@ FileBrowser* file_browser_alloc(FuriString* result_path) { void file_browser_free(FileBrowser* browser) { furi_assert(browser); + furi_timer_free(browser->scroll_timer); + with_view_model( browser->view, FileBrowserModel * model, { items_array_clear(model->items); }, false); @@ -468,13 +501,17 @@ static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) { furi_string_set(filename, ". ."); } - elements_string_fit_width( - canvas, filename, (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX)); - + size_t scroll_counter = model->scroll_counter; if(model->item_idx == idx) { browser_draw_frame(canvas, i, show_scrollbar); + if(scroll_counter < SCROLL_DELAY) { + scroll_counter = 0; + } else { + scroll_counter -= SCROLL_DELAY; + } } else { canvas_set_color(canvas, ColorBlack); + scroll_counter = 0; } if(custom_icon_data) { @@ -487,8 +524,14 @@ static void browser_draw_list(Canvas* canvas, FileBrowserModel* model) { canvas_draw_icon( canvas, 2, Y_OFFSET + 1 + i * FRAME_HEIGHT, BrowserItemIcons[item_type]); } - canvas_draw_str( - canvas, 15, Y_OFFSET + 9 + i * FRAME_HEIGHT, furi_string_get_cstr(filename)); + elements_scrollable_text_line( + canvas, + 15, + Y_OFFSET + 9 + i * FRAME_HEIGHT, + (show_scrollbar ? MAX_LEN_PX - 6 : MAX_LEN_PX), + filename, + scroll_counter, + (model->item_idx != idx)); } if(show_scrollbar) { @@ -543,6 +586,7 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) { file_browser_worker_load( browser->worker, load_offset, ITEM_LIST_LEN_MAX); } + model->scroll_counter = 0; } else if(event->key == InputKeyDown) { model->item_idx = (model->item_idx + 1) % model->item_cnt; if(browser_is_list_load_required(model)) { @@ -554,6 +598,7 @@ static bool file_browser_view_input_callback(InputEvent* event, void* context) { file_browser_worker_load( browser->worker, load_offset, ITEM_LIST_LEN_MAX); } + model->scroll_counter = 0; } }, true); diff --git a/firmware/targets/f7/api_symbols.csv b/firmware/targets/f7/api_symbols.csv index d8db33a3..ada82685 100644 --- a/firmware/targets/f7/api_symbols.csv +++ b/firmware/targets/f7/api_symbols.csv @@ -752,6 +752,7 @@ Function,+,elements_multiline_text,void,"Canvas*, uint8_t, uint8_t, const char*" Function,+,elements_multiline_text_aligned,void,"Canvas*, uint8_t, uint8_t, Align, Align, const char*" Function,+,elements_multiline_text_framed,void,"Canvas*, uint8_t, uint8_t, const char*" Function,+,elements_progress_bar,void,"Canvas*, uint8_t, uint8_t, uint8_t, float" +Function,+,elements_scrollable_text_line,void,"Canvas*, uint8_t, uint8_t, uint8_t, FuriString*, size_t, _Bool" Function,+,elements_scrollbar,void,"Canvas*, uint16_t, uint16_t" Function,+,elements_scrollbar_pos,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint16_t, uint16_t" Function,+,elements_slightly_rounded_box,void,"Canvas*, uint8_t, uint8_t, uint8_t, uint8_t"