From 688302a3b3cee640c268ba767b469fec5bfa2ca8 Mon Sep 17 00:00:00 2001 From: Thomas Martitz Date: Sun, 8 Apr 2012 13:15:39 +0200 Subject: touchscreen: Rewrite kinetic scrolling using a state machine. The old code was very confusing. The scrolling modes need to be handled differently, thus a state machine makes sense. Should fix numerious glitches and be easier to maintain. NOTE: Behavior is still a bit glitchy with an SBS in use, because the skin engine sees the touch events earlier than the list code. Change-Id: I4ccead359c81de0d0fc3dea636fe2cb3a28d1bc6 --- apps/gui/bitmap/list.c | 416 +++++++++++++++++++++++++------------------------ 1 file changed, 213 insertions(+), 203 deletions(-) diff --git a/apps/gui/bitmap/list.c b/apps/gui/bitmap/list.c index 2f13d8ba53..05a7f066af 100644 --- a/apps/gui/bitmap/list.c +++ b/apps/gui/bitmap/list.c @@ -48,17 +48,8 @@ static struct viewport list_text[NB_SCREENS], title_text[NB_SCREENS]; #ifdef HAVE_TOUCHSCREEN -/* difference in pixels between draws, above it means enough to start scrolling */ -#define SCROLL_BEGIN_THRESHOLD 3 - -static enum { - SCROLL_NONE, /* no scrolling */ - SCROLL_BAR, /* scroll by using the scrollbar */ - SCROLL_SWIPE, /* scroll by wiping over the screen */ - SCROLL_KINETIC, /* state after releasing swipe */ -} scroll_mode; - static int y_offset; +static bool hide_selection = true; #endif int gui_list_get_item_offset(struct gui_synclist * gui_list, int item_width, @@ -90,6 +81,17 @@ void gui_synclist_scroll_stop(struct gui_synclist *lists) Note: This image is flipped horizontally when the language is a right-to-left one (Hebrew, Arabic) */ + +static int list_icon_width(enum screen_type screen) +{ + return get_icon_width(screen) + ICON_PADDING * 2; +} + +static int list_icon_height(enum screen_type screen) +{ + return get_icon_height(screen); +} + static bool draw_title(struct screen *display, struct gui_synclist *list) { const int screen = display->screen_type; @@ -108,9 +110,9 @@ static bool draw_title(struct screen *display, struct gui_synclist *list) { struct viewport title_icon = *title_text_vp; - title_icon.width = get_icon_width(screen) + ICON_PADDING * 2; - title_icon.y += (title_icon.height - get_icon_height(screen)) / 2; - title_icon.height = get_icon_height(screen); + title_icon.width = list_icon_width(screen); + title_icon.y += (title_icon.height - list_icon_height(screen)) / 2; + title_icon.height = list_icon_height(screen); if (VP_IS_RTL(&title_icon)) { title_icon.x += title_text_vp->width - title_icon.width; @@ -141,7 +143,7 @@ void list_draw(struct screen *display, struct gui_synclist *list) int start, end, line_height, style, item_offset, i; const int screen = display->screen_type; const int list_start_item = list->start_item[screen]; - const int icon_width = get_icon_width(screen) + ICON_PADDING; + const int icon_width = list_icon_width(screen); const bool scrollbar_in_left = (global_settings.scrollbar == SCROLLBAR_LEFT); const bool show_cursor = !global_settings.cursor_style && list->show_selection_marker; @@ -236,7 +238,7 @@ void list_draw(struct screen *display, struct gui_synclist *list) list_icons.x += list_text_vp->width + ICON_PADDING; else list_text_vp->x += list_icons.width + ICON_PADDING; - icon_yoffset = (line_height - get_icon_height(screen)) / 2; + icon_yoffset = (line_height - list_icon_height(screen)) / 2; } for (i=start; inb_items; i++) @@ -274,7 +276,7 @@ void list_draw(struct screen *display, struct gui_synclist *list) if( #ifdef HAVE_TOUCHSCREEN /* don't draw it during scrolling */ - scroll_mode == SCROLL_NONE && + !hide_selection && #endif i >= list->selected_item && i < list->selected_item + list->selected_size @@ -338,7 +340,7 @@ void list_draw(struct screen *display, struct gui_synclist *list) display->set_viewport(&list_icons); if (list->callback_get_item_icon != NULL) { - int xoff = show_cursor ? get_icon_width(screen) + ICON_PADDING : 0; + int xoff = show_cursor ? list_icon_width(screen) : 0; screen_put_iconxy(display, xoff, line*line_height + draw_offset + icon_yoffset, list->callback_get_item_icon(i, list->data)); @@ -360,13 +362,15 @@ void list_draw(struct screen *display, struct gui_synclist *list) #if defined(HAVE_TOUCHSCREEN) /* This needs to be fixed if we ever get more than 1 touchscreen on a target. */ -static bool released = false; +/* difference in pixels between draws, above it means enough to start scrolling */ +#define SCROLL_BEGIN_THRESHOLD 3 -/* Used for kinetic scrolling as we need to know the last position to - * recognize the scroll direction. - * This gets reset to 0 at the end of scrolling - */ -static int last_position=0; +static enum { + SCROLL_NONE, /* no scrolling */ + SCROLL_BAR, /* scroll by using the scrollbar */ + SCROLL_SWIPE, /* scroll by wiping over the screen */ + SCROLL_KINETIC, /* state after releasing swipe */ +} scroll_mode; static int scrollbar_scroll(struct gui_synclist * gui_list, int y) @@ -486,6 +490,7 @@ void _gui_synclist_stop_kinetic_scrolling(void) if (scroll_mode == SCROLL_KINETIC) kinetic_force_stop(); scroll_mode = SCROLL_NONE; + hide_selection = false; } /* * returns false if scrolling should be stopped entirely @@ -497,11 +502,12 @@ void _gui_synclist_stop_kinetic_scrolling(void) static int scroll_begin_threshold; static int threshold_accumulation; -static bool swipe_scroll(struct gui_synclist * gui_list, int line_height, int difference) +static bool swipe_scroll(struct gui_synclist * gui_list, int difference) { /* fixme */ const enum screen_type screen = screens[SCREEN_MAIN].screen_type; const int nb_lines = viewport_get_nb_lines(&list_text[screen]); + const int line_height = gui_list->parent[0]->line_height; if (UNLIKELY(scroll_begin_threshold == 0)) scroll_begin_threshold = touchscreen_get_scroll_threshold(); @@ -566,13 +572,12 @@ static int kinetic_callback(struct timeout *tmo) return 0; struct cb_data *data = (struct cb_data*)tmo->data; - int line_height = data->list->parent[0]->line_height; /* ds = v*dt */ int pixel_diff = data->velocity * RELOAD_INTERVAL / HZ; /* remember signedness to detect stopping */ int old_sign = SIGN(data->velocity); /* advance the list */ - if (!swipe_scroll(data->list, line_height, pixel_diff)) + if (!swipe_scroll(data->list, pixel_diff)) { /* nothing to scroll? */ data->velocity = 0; @@ -587,6 +592,8 @@ static int kinetic_callback(struct timeout *tmo) data->velocity = 0; } + /* let get_action() timeout, which loads to a + * gui_synclist_draw() call from the main thread */ queue_post(&button_queue, BUTTON_REDRAW, 0); /* stop if the velocity hit or crossed zero */ if (!data->velocity) @@ -594,8 +601,6 @@ static int kinetic_callback(struct timeout *tmo) kinetic_stats_reset(); return 0; } - /* let get_action() timeout, which loads to a - * gui_synclist_draw() call from the main thread */ return RELOAD_INTERVAL; /* cancel or reload */ } @@ -628,208 +633,213 @@ static bool kinetic_setup_scroll(struct gui_synclist *list) return false; } -unsigned gui_synclist_do_touchscreen(struct gui_synclist * gui_list) -{ - short x, y; - const enum screen_type screen = SCREEN_MAIN; - struct viewport *parent = gui_list->parent[screen]; - const int button = action_get_touchscreen_press_in_vp(&x, &y, parent); - const int list_start_item = gui_list->start_item[screen]; - const int line_height = gui_list->parent[screen]->line_height; - const struct viewport *list_text_vp = &list_text[screen]; - const bool old_released = released; - const bool show_title = list_display_title(gui_list, screen); - const bool show_cursor = !global_settings.cursor_style && - gui_list->show_selection_marker; - const bool on_title_clicked = show_title && y < line_height && (button&BUTTON_REL); - const bool cancelled_kinetic = (scroll_mode == SCROLL_KINETIC - && button != ACTION_NONE && button != ACTION_UNKNOWN - && !is_kinetic_over()); - int icon_width = 0; - int line, list_width = list_text_vp->width; +#define OUTSIDE 0 +#define TITLE_TEXT (1<<0) +#define TITLE_ICON (1<<1) +#define SCROLLBAR (1<<2) +#define LIST_TEXT (1<<3) +#define LIST_ICON (1<<4) - released = (button&BUTTON_REL) != 0; - - if (button == ACTION_NONE || button == ACTION_UNKNOWN) - { - /* this happens when we hit edges of the list while kinetic scrolling, - * but not when manually cancelling */ - if (scroll_mode == SCROLL_KINETIC) - return ACTION_REDRAW; - return ACTION_NONE; - } +#define TITLE (TITLE_TEXT|TITLE_ICON) +#define LIST (LIST_TEXT|LIST_ICON) - /* x and y are relative to parent */ - if (gui_list->callback_get_item_icon != NULL) - icon_width += get_icon_width(screen); - if (show_cursor) - icon_width += get_icon_width(screen); +static int get_click_location(struct gui_synclist *list, int x, int y) +{ + int screen = SCREEN_MAIN; + struct viewport *parent, *title, *text; + int retval = OUTSIDE; - if (on_title_clicked) + parent = list->parent[screen]; + if (viewport_point_within_vp(parent, x, y)) { - if (scroll_mode == SCROLL_NONE || is_kinetic_over()) + /* see if the title was clicked */ + title = &title_text[screen]; + if (viewport_point_within_vp(title, x, y)) + retval = TITLE_TEXT; + /* check the icon too */ + if (list->title_icon != Icon_NOICON && global_settings.show_icons) + { + int width = list_icon_width(screen); + struct viewport vp = *title; + if (VP_IS_RTL(&vp)) + vp.x += vp.width; + else + vp.x -= width; + vp.width = width; + if (viewport_point_within_vp(&vp, x, y)) + retval = TITLE_ICON; + } + /* check scrollbar. assume it's shown, if it isn't it will be handled + * later */ + if (retval == OUTSIDE) { - if (x < icon_width) + bool on_scrollbar_clicked; + int adj_x = x - parent->x; + switch (global_settings.scrollbar) { - /* Top left corner is GO_TO_ROOT */ - if (button == BUTTON_REL) - return ACTION_STD_MENU; - else if (button == (BUTTON_REPEAT|BUTTON_REL)) - return ACTION_STD_CONTEXT; - return ACTION_NONE; + case SCROLLBAR_LEFT: + on_scrollbar_clicked = adj_x <= SCROLLBAR_WIDTH; break; + case SCROLLBAR_RIGHT: + on_scrollbar_clicked = adj_x > (title->x + title->width - SCROLLBAR_WIDTH); break; + default: + on_scrollbar_clicked = false; break; } - else /* click on title text is cancel */ - if (button == BUTTON_REL) - return ACTION_STD_CANCEL; + if (on_scrollbar_clicked) + retval = SCROLLBAR; } - /* do this after the above so the scrolling stops without - * going back in the list with the same touch */ - if (scroll_mode == SCROLL_KINETIC) + if (retval == OUTSIDE) { - kinetic_force_stop(); - scroll_mode = SCROLL_NONE; + text = &list_text[screen]; + if (viewport_point_within_vp(text, x, y)) + retval = LIST_TEXT; + else /* if all fails, it must be on the list icons */ + retval = LIST_ICON; } } - else /* list area clicked (or not released) */ - { - const int actual_y = y - (show_title ? line_height : 0); - bool on_scrollbar_clicked; - switch (global_settings.scrollbar) - { - case SCROLLBAR_LEFT: - on_scrollbar_clicked = x <= SCROLLBAR_WIDTH; break; - case SCROLLBAR_RIGHT: - on_scrollbar_clicked = x > (icon_width + list_width); break; - default: - on_scrollbar_clicked = false; break; - } - /* conditions for scrollbar scrolling: - * * pen is on the scrollbar - * AND scrollbar is on the right (left case is handled above) - * OR * pen is in the somewhere else but we did scrollbar scrolling before - * - * scrollbar scrolling must end if the pen is released - * scrollbar scrolling must not happen if we're currently scrolling - * via swiping the screen - **/ - - if (!released && scroll_mode != SCROLL_SWIPE && - (on_scrollbar_clicked || scroll_mode == SCROLL_BAR)) - { - if (scroll_mode == SCROLL_KINETIC) - kinetic_force_stop(); - scroll_mode = SCROLL_BAR; - return scrollbar_scroll(gui_list, y); - } + return retval; +} - /* |--------------------------------------------------------| - * | Description of the touchscreen list interface: | - * |--------------------------------------------------------| - * | Pressing an item will select it and "enter" it. | - * | | - * | Pressing and holding your pen down will scroll through | - * | the list of items. | - * | | - * | Pressing and holding your pen down on a single item | - * | will bring up the context menu of it. | - * |--------------------------------------------------------| - */ - if (actual_y > 0 || button & BUTTON_REPEAT) +unsigned gui_synclist_do_touchscreen(struct gui_synclist * list) +{ + enum screen_type screen; + struct viewport *parent; + short x, y; + int action, adj_y, line, line_height, list_start_item; + bool recurse; + static int last_y; + + screen = SCREEN_MAIN; + parent = list->parent[screen]; + line_height = list->parent[screen]->line_height; + list_start_item = list->start_item[screen]; + /* start with getting the action code and finding the click location */ + action = action_get_touchscreen_press(&x, &y); + adj_y = y - parent->y; + + /* selection needs to be corrected if items are only partially visible */ + line = (adj_y - y_offset) / line_height; + if (list_display_title(list, screen)) + line -= 1; /* adjust for the list title */ + + /* some defaults before running the state machine */ + recurse = false; + hide_selection = true; + + switch (scroll_mode) + { + case SCROLL_NONE: { - /* selection needs to be corrected if an items are only - * partially visible */ - line = (actual_y - y_offset) / line_height; - - if (cancelled_kinetic) - { - kinetic_force_stop(); - scroll_mode = SCROLL_SWIPE; - } - - /* Pressed below the list*/ - if (list_start_item + line >= gui_list->nb_items) - { - /* don't collect last_position outside of the list area - * it'd break selecting after such a situation */ - last_position = 0; - return ACTION_NONE; + list->selected_item = list_start_item+line; + gui_synclist_speak_item(list); + if (!last_y) + { /* first run. register adj_y and re-run (will then take the else case) */ + last_y = adj_y; + recurse = true; } - - if (button & BUTTON_REPEAT && scroll_mode == SCROLL_NONE) - { - /* held a single line for a while, bring up the context menu */ - gui_synclist_select_item(gui_list, list_start_item + line); - /* don't sent context repeatedly */ - action_wait_for_release(); - last_position = 0; - return ACTION_STD_CONTEXT; - } - if (released && !cancelled_kinetic) + else { - /* Pen was released anywhere on the screen */ - last_position = 0; - if (scroll_mode == SCROLL_NONE) + int click_loc = get_click_location(list, x, y); + if (action == BUTTON_TOUCHSCREEN) { - /* select current line */ - gui_synclist_select_item(gui_list, list_start_item + line); - return ACTION_STD_OK; + /* if not scrolling, the user is trying to select */ + int diff = adj_y - last_y; + if ((click_loc & LIST) && swipe_scroll(list, diff)) + scroll_mode = SCROLL_SWIPE; + else if (click_loc & SCROLLBAR) + scroll_mode = SCROLL_BAR; + + hide_selection = click_loc & SCROLLBAR; } - else + else if (action == BUTTON_REPEAT) { - /* we were scrolling - * -> reset scrolling but do nothing else */ - if (scroll_mode == SCROLL_SWIPE) + if (click_loc & LIST) { - if (kinetic_setup_scroll(gui_list)) - scroll_mode = SCROLL_KINETIC; + /* held a single line for a while, bring up the context menu */ + gui_synclist_select_item(list, list_start_item + line); + /* don't sent context repeatedly */ + action_wait_for_release(); + last_y = 0; + return ACTION_STD_CONTEXT; } - if (scroll_mode != SCROLL_KINETIC) - scroll_mode = SCROLL_NONE; - return ACTION_NONE; } - } - else - { /* pen is on the screen */ - bool redraw = false, result = false; - /* beginning of list interaction denoted by release in - * the previous call */ - if (old_released || is_kinetic_over()) + else if (action & BUTTON_REL) { - scroll_mode = SCROLL_NONE; - redraw = true; - } - - /* select current item; gui_synclist_select_item() - * is not called because it has side effects that - * disturb kinetic scrolling */ - gui_list->selected_item = list_start_item+line; - gui_synclist_speak_item(gui_list); - if (last_position == 0) - { - redraw = true; - last_position = actual_y; - } - else - { - /* record speed data in case we do kinetic scrolling */ - int diff = actual_y - last_position; - kinetic_stats_collect(diff); - result = swipe_scroll(gui_list, line_height, diff); + last_y = 0; + if (click_loc & LIST) + { /* release on list item enters it */ + gui_synclist_select_item(list, list_start_item + line); + return ACTION_STD_OK; + } + else if (click_loc & TITLE_TEXT) + { /* clicking the title goes one level up (cancel) */ + return ACTION_STD_CANCEL; + } + else if (click_loc & TITLE_ICON) + { /* clicking the title icon goes back to the root */ + return ACTION_STD_MENU; + } } - - /* Start scrolling once the pen is moved without - * releasing it inbetween */ - if (result) + } + break; + } + case SCROLL_SWIPE: + { + /* when swipe scrolling, we accept outside presses as well and + * grab the entire screen (i.e. click_loc does not matter) */ + int diff = adj_y - last_y; + kinetic_stats_collect(diff); + if (swipe_scroll(list, diff)) + { + /* letting the pen go enters kinetic scrolling */ + if ((action & BUTTON_REL)) { - redraw = true; - scroll_mode = SCROLL_SWIPE; + if (kinetic_setup_scroll(list)) + scroll_mode = SCROLL_KINETIC; + else + scroll_mode = SCROLL_NONE; } - last_position = actual_y; - - return redraw ? ACTION_REDRAW:ACTION_NONE; } + else if (action & BUTTON_REL) + { + scroll_mode = SCROLL_NONE; + } + break; + } + case SCROLL_KINETIC: + { + /* during kinetic scrolling we need to handle cancellation. + * This state is actually only entered upon end of it as this + * function is not called during the animation. */ + if (!is_kinetic_over()) + { /* a) the user touched the screen (manual cancellation) */ + kinetic_force_stop(); + scroll_mode = SCROLL_SWIPE; + } + else + { /* b) kinetic scrolling stopped on its own */ + /* need to re-run this with SCROLL_NONE since otherwise + * the next touch is not detected correctly */ + scroll_mode = SCROLL_NONE; + recurse = true; + } + break; + } + case SCROLL_BAR: + { + /* similarly to swipe scroll, using the scrollbar grabs + * focus so the click location is irrelevant */ + scrollbar_scroll(list, adj_y); + if (action & BUTTON_REL) + scroll_mode = SCROLL_NONE; + break; } } - return ACTION_REDRAW; + + /* register y position unless forcefully reset to 0 */ + if (last_y) + last_y = adj_y; + + return recurse ? gui_synclist_do_touchscreen(list) : ACTION_REDRAW; } + #endif -- cgit v1.2.3