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
This commit is contained in:
Thomas Martitz 2012-04-08 13:15:39 +02:00
parent a12b2a5df9
commit 688302a3b3

View file

@ -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; i<end && i<list->nb_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)
#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)
#define TITLE (TITLE_TEXT|TITLE_ICON)
#define LIST (LIST_TEXT|LIST_ICON)
static int get_click_location(struct gui_synclist *list, int x, int y)
{
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;
int screen = SCREEN_MAIN;
struct viewport *parent, *title, *text;
int retval = OUTSIDE;
released = (button&BUTTON_REL) != 0;
if (button == ACTION_NONE || button == ACTION_UNKNOWN)
parent = list->parent[screen];
if (viewport_point_within_vp(parent, x, y))
{
/* 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;
}
/* 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);
if (on_title_clicked)
{
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)
{
if (x < icon_width)
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)
{
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) */
return retval;
}
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)
{
const int actual_y = y - (show_title ? line_height : 0);
bool on_scrollbar_clicked;
switch (global_settings.scrollbar)
case SCROLL_NONE:
{
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);
}
/* |--------------------------------------------------------|
* | 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)
{
/* selection needs to be corrected if an items are only
* partially visible */
line = (actual_y - y_offset) / line_height;
if (cancelled_kinetic)
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;
}
else
{
int click_loc = get_click_location(list, x, y);
if (action == BUTTON_TOUCHSCREEN)
{
/* 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 if (action == BUTTON_REPEAT)
{
if (click_loc & LIST)
{
/* 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;
}
}
else if (action & BUTTON_REL)
{
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;
}
}
}
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))
{
if (kinetic_setup_scroll(list))
scroll_mode = SCROLL_KINETIC;
else
scroll_mode = SCROLL_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;
}
/* 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;
}
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)
{
/* Pen was released anywhere on the screen */
last_position = 0;
if (scroll_mode == SCROLL_NONE)
{
/* select current line */
gui_synclist_select_item(gui_list, list_start_item + line);
return ACTION_STD_OK;
}
else
{
/* we were scrolling
* -> reset scrolling but do nothing else */
if (scroll_mode == SCROLL_SWIPE)
{
if (kinetic_setup_scroll(gui_list))
scroll_mode = SCROLL_KINETIC;
}
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())
{
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);
}
/* Start scrolling once the pen is moved without
* releasing it inbetween */
if (result)
{
redraw = true;
scroll_mode = SCROLL_SWIPE;
}
last_position = actual_y;
return redraw ? ACTION_REDRAW:ACTION_NONE;
{ /* 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