/** * @file lv_gridnav.c * */ /********************* * INCLUDES *********************/ #include "lv_gridnav.h" #if LV_USE_GRIDNAV #include "../../../misc/lv_assert.h" #include "../../../misc/lv_math.h" #include "../../../core/lv_indev.h" /********************* * DEFINES *********************/ /********************** * TYPEDEFS **********************/ typedef struct { lv_gridnav_ctrl_t ctrl; lv_obj_t * focused_obj; } lv_gridnav_dsc_t; typedef enum { FIND_LEFT, FIND_RIGHT, FIND_TOP, FIND_BOTTOM, FIND_NEXT_ROW_FIRST_ITEM, FIND_PREV_ROW_LAST_ITEM, FIND_FIRST_ROW, FIND_LAST_ROW, } find_mode_t; /********************** * STATIC PROTOTYPES **********************/ static void gridnav_event_cb(lv_event_t * e); static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode); static lv_obj_t * find_first_focusable(lv_obj_t * obj); static lv_obj_t * find_last_focusable(lv_obj_t * obj); static bool obj_is_focuable(lv_obj_t * obj); static lv_coord_t get_x_center(lv_obj_t * obj); static lv_coord_t get_y_center(lv_obj_t * obj); /********************** * STATIC VARIABLES **********************/ /********************** * MACROS **********************/ /********************** * GLOBAL FUNCTIONS **********************/ void lv_gridnav_add(lv_obj_t * obj, lv_gridnav_ctrl_t ctrl) { lv_gridnav_remove(obj); /*Be sure to not add gridnav twice*/ lv_gridnav_dsc_t * dsc = lv_mem_alloc(sizeof(lv_gridnav_dsc_t)); LV_ASSERT_MALLOC(dsc); dsc->ctrl = ctrl; dsc->focused_obj = NULL; lv_obj_add_event_cb(obj, gridnav_event_cb, LV_EVENT_ALL, dsc); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLL_WITH_ARROW); } void lv_gridnav_remove(lv_obj_t * obj) { lv_gridnav_dsc_t * dsc = lv_obj_get_event_user_data(obj, gridnav_event_cb); if(dsc == NULL) return; /* no gridnav on this object */ lv_mem_free(dsc); lv_obj_remove_event_cb(obj, gridnav_event_cb); } void lv_gridnav_set_focused(lv_obj_t * cont, lv_obj_t * to_focus, lv_anim_enable_t anim_en) { LV_ASSERT_NULL(to_focus); lv_gridnav_dsc_t * dsc = lv_obj_get_event_user_data(cont, gridnav_event_cb); if(dsc == NULL) { LV_LOG_WARN("`cont` is not a gridnav container"); return; } if(obj_is_focuable(to_focus) == false) { LV_LOG_WARN("The object to focus is not focusable"); return; } lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_add_state(to_focus, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_scroll_to_view(to_focus, anim_en); dsc->focused_obj = to_focus; } /********************** * STATIC FUNCTIONS **********************/ static void gridnav_event_cb(lv_event_t * e) { lv_obj_t * obj = lv_event_get_current_target(e); lv_gridnav_dsc_t * dsc = lv_event_get_user_data(e); lv_event_code_t code = lv_event_get_code(e); if(code == LV_EVENT_KEY) { uint32_t child_cnt = lv_obj_get_child_cnt(obj); if(child_cnt == 0) return; if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj); if(dsc->focused_obj == NULL) return; uint32_t key = lv_event_get_key(e); lv_obj_t * guess = NULL; if(key == LV_KEY_RIGHT) { if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) && lv_obj_get_scroll_right(dsc->focused_obj) > 0) { lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4; if(d <= 0) d = 1; lv_obj_scroll_by_bounded(dsc->focused_obj, -d, 0, LV_ANIM_ON); } else { guess = find_chid(obj, dsc->focused_obj, FIND_RIGHT); if(guess == NULL) { if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) { guess = find_chid(obj, dsc->focused_obj, FIND_NEXT_ROW_FIRST_ITEM); if(guess == NULL) guess = find_first_focusable(obj); } else { lv_group_focus_next(lv_obj_get_group(obj)); } } } } else if(key == LV_KEY_LEFT) { if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) && lv_obj_get_scroll_left(dsc->focused_obj) > 0) { lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4; if(d <= 0) d = 1; lv_obj_scroll_by_bounded(dsc->focused_obj, d, 0, LV_ANIM_ON); } else { guess = find_chid(obj, dsc->focused_obj, FIND_LEFT); if(guess == NULL) { if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) { guess = find_chid(obj, dsc->focused_obj, FIND_PREV_ROW_LAST_ITEM); if(guess == NULL) guess = find_last_focusable(obj); } else { lv_group_focus_prev(lv_obj_get_group(obj)); } } } } else if(key == LV_KEY_DOWN) { if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) && lv_obj_get_scroll_bottom(dsc->focused_obj) > 0) { lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4; if(d <= 0) d = 1; lv_obj_scroll_by_bounded(dsc->focused_obj, 0, -d, LV_ANIM_ON); } else { guess = find_chid(obj, dsc->focused_obj, FIND_BOTTOM); if(guess == NULL) { if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) { guess = find_chid(obj, dsc->focused_obj, FIND_FIRST_ROW); } else { lv_group_focus_next(lv_obj_get_group(obj)); } } } } else if(key == LV_KEY_UP) { if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) && lv_obj_get_scroll_top(dsc->focused_obj) > 0) { lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4; if(d <= 0) d = 1; lv_obj_scroll_by_bounded(dsc->focused_obj, 0, d, LV_ANIM_ON); } else { guess = find_chid(obj, dsc->focused_obj, FIND_TOP); if(guess == NULL) { if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) { guess = find_chid(obj, dsc->focused_obj, FIND_LAST_ROW); } else { lv_group_focus_prev(lv_obj_get_group(obj)); } } } } else { if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) { lv_event_send(dsc->focused_obj, LV_EVENT_KEY, &key); } } if(guess && guess != dsc->focused_obj) { lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_add_state(guess, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_scroll_to_view(guess, LV_ANIM_ON); dsc->focused_obj = guess; } } else if(code == LV_EVENT_FOCUSED) { if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj); if(dsc->focused_obj) { lv_obj_add_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_scroll_to_view(dsc->focused_obj, LV_ANIM_OFF); } } else if(code == LV_EVENT_DEFOCUSED) { if(dsc->focused_obj) { lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); } } else if(code == LV_EVENT_CHILD_CREATED) { lv_obj_t * child = lv_event_get_target(e); if(lv_obj_get_parent(child) == obj) { if(dsc->focused_obj == NULL) { dsc->focused_obj = child; if(lv_obj_has_state(obj, LV_STATE_FOCUSED)) { lv_obj_add_state(child, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY); lv_obj_scroll_to_view(child, LV_ANIM_OFF); } } } } else if(code == LV_EVENT_CHILD_DELETED) { /*This event bubble, so be sure this object's child was deleted. *As we don't know which object was deleted we can't make the next focused. *So make the first object focused*/ lv_obj_t * target = lv_event_get_target(e); if(target == obj) { dsc->focused_obj = find_first_focusable(obj); } } else if(code == LV_EVENT_DELETE) { lv_gridnav_remove(obj); } else if(code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_PRESS_LOST || code == LV_EVENT_LONG_PRESSED || code == LV_EVENT_LONG_PRESSED_REPEAT || code == LV_EVENT_CLICKED || code == LV_EVENT_RELEASED) { if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) { /*Forward press/release related event too*/ lv_indev_type_t t = lv_indev_get_type(lv_indev_get_act()); if(t == LV_INDEV_TYPE_ENCODER || t == LV_INDEV_TYPE_KEYPAD) { lv_event_send(dsc->focused_obj, code, lv_indev_get_act()); } } } } static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode) { lv_coord_t x_start = get_x_center(start_child); lv_coord_t y_start = get_y_center(start_child); uint32_t child_cnt = lv_obj_get_child_cnt(obj); lv_obj_t * guess = NULL; lv_coord_t x_err_guess = LV_COORD_MAX; lv_coord_t y_err_guess = LV_COORD_MAX; lv_coord_t h_half = lv_obj_get_height(start_child) / 2; lv_coord_t h_max = lv_obj_get_height(obj) + lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj); uint32_t i; for(i = 0; i < child_cnt; i++) { lv_obj_t * child = lv_obj_get_child(obj, i); if(child == start_child) continue; if(obj_is_focuable(child) == false) continue; lv_coord_t x_err = 0; lv_coord_t y_err = 0; switch(mode) { case FIND_LEFT: x_err = get_x_center(child) - x_start; y_err = get_y_center(child) - y_start; if(x_err >= 0) continue; /*It's on the right*/ if(LV_ABS(y_err) > h_half) continue; /*Too far*/ break; case FIND_RIGHT: x_err = get_x_center(child) - x_start; y_err = get_y_center(child) - y_start; if(x_err <= 0) continue; /*It's on the left*/ if(LV_ABS(y_err) > h_half) continue; /*Too far*/ break; case FIND_TOP: x_err = get_x_center(child) - x_start; y_err = get_y_center(child) - y_start; if(y_err >= 0) continue; /*It's on the bottom*/ break; case FIND_BOTTOM: x_err = get_x_center(child) - x_start; y_err = get_y_center(child) - y_start; if(y_err <= 0) continue; /*It's on the top*/ break; case FIND_NEXT_ROW_FIRST_ITEM: y_err = get_y_center(child) - y_start; if(y_err <= 0) continue; /*It's on the top*/ x_err = lv_obj_get_x(child); break; case FIND_PREV_ROW_LAST_ITEM: y_err = get_y_center(child) - y_start; if(y_err >= 0) continue; /*It's on the bottom*/ x_err = obj->coords.x2 - child->coords.x2; break; case FIND_FIRST_ROW: x_err = get_x_center(child) - x_start; y_err = lv_obj_get_y(child); break; case FIND_LAST_ROW: x_err = get_x_center(child) - x_start; y_err = h_max - lv_obj_get_y(child); } if(guess == NULL || (y_err * y_err + x_err * x_err < y_err_guess * y_err_guess + x_err_guess * x_err_guess)) { guess = child; x_err_guess = x_err; y_err_guess = y_err; } } return guess; } static lv_obj_t * find_first_focusable(lv_obj_t * obj) { uint32_t child_cnt = lv_obj_get_child_cnt(obj); uint32_t i; for(i = 0; i < child_cnt; i++) { lv_obj_t * child = lv_obj_get_child(obj, i); if(obj_is_focuable(child)) return child; } return NULL; } static lv_obj_t * find_last_focusable(lv_obj_t * obj) { uint32_t child_cnt = lv_obj_get_child_cnt(obj); int32_t i; for(i = child_cnt - 1; i >= 0; i++) { lv_obj_t * child = lv_obj_get_child(obj, i); if(obj_is_focuable(child)) return child; } return NULL; } static bool obj_is_focuable(lv_obj_t * obj) { if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return false; if(lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)) return true; else return false; } static lv_coord_t get_x_center(lv_obj_t * obj) { return obj->coords.x1 + lv_area_get_width(&obj->coords) / 2; } static lv_coord_t get_y_center(lv_obj_t * obj) { return obj->coords.y1 + lv_area_get_height(&obj->coords) / 2; } #endif /*LV_USE_GRIDNAV*/