1 /**
2  * @file lv_gridnav.c
3  *
4  */
5 
6 /*********************
7  *      INCLUDES
8  *********************/
9 #include "lv_gridnav.h"
10 #if LV_USE_GRIDNAV
11 
12 #include "../../misc/lv_assert.h"
13 #include "../../misc/lv_math.h"
14 #include "../../indev/lv_indev.h"
15 #include "../../core/lv_obj_private.h"
16 
17 /*********************
18  *      DEFINES
19  *********************/
20 
21 /**********************
22  *      TYPEDEFS
23  **********************/
24 typedef struct {
25     lv_gridnav_ctrl_t ctrl;
26     lv_obj_t * focused_obj;
27 } lv_gridnav_dsc_t;
28 
29 typedef enum {
30     FIND_LEFT,
31     FIND_RIGHT,
32     FIND_TOP,
33     FIND_BOTTOM,
34     FIND_NEXT_ROW_FIRST_ITEM,
35     FIND_PREV_ROW_LAST_ITEM,
36     FIND_FIRST_ROW,
37     FIND_LAST_ROW,
38 } find_mode_t;
39 
40 /**********************
41  *  STATIC PROTOTYPES
42  **********************/
43 static void gridnav_event_cb(lv_event_t * e);
44 static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode);
45 static lv_obj_t * find_first_focusable(lv_obj_t * obj);
46 static lv_obj_t * find_last_focusable(lv_obj_t * obj);
47 static bool obj_is_focusable(lv_obj_t * obj);
48 static int32_t get_x_center(lv_obj_t * obj);
49 static int32_t get_y_center(lv_obj_t * obj);
50 
51 /**********************
52  *  STATIC VARIABLES
53  **********************/
54 
55 /**********************
56  *      MACROS
57  **********************/
58 
59 /**********************
60  *   GLOBAL FUNCTIONS
61  **********************/
62 
lv_gridnav_add(lv_obj_t * obj,lv_gridnav_ctrl_t ctrl)63 void lv_gridnav_add(lv_obj_t * obj, lv_gridnav_ctrl_t ctrl)
64 {
65     lv_gridnav_remove(obj); /*Be sure to not add gridnav twice*/
66 
67     lv_gridnav_dsc_t * dsc = lv_malloc(sizeof(lv_gridnav_dsc_t));
68     LV_ASSERT_MALLOC(dsc);
69     dsc->ctrl = ctrl;
70     dsc->focused_obj = NULL;
71     lv_obj_add_event_cb(obj, gridnav_event_cb, LV_EVENT_ALL, dsc);
72 
73     lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLL_WITH_ARROW);
74 }
75 
lv_gridnav_remove(lv_obj_t * obj)76 void lv_gridnav_remove(lv_obj_t * obj)
77 {
78     lv_event_dsc_t * event_dsc = NULL;
79     uint32_t event_cnt = lv_obj_get_event_count(obj);
80     uint32_t i;
81     for(i = 0; i < event_cnt; i++) {
82         event_dsc = lv_obj_get_event_dsc(obj, i);
83         if(lv_event_dsc_get_cb(event_dsc) == gridnav_event_cb) {
84             lv_free(lv_event_dsc_get_user_data(event_dsc));
85             lv_obj_remove_event(obj, i);
86             break;
87         }
88     }
89 
90 }
91 
lv_gridnav_set_focused(lv_obj_t * cont,lv_obj_t * to_focus,lv_anim_enable_t anim_en)92 void lv_gridnav_set_focused(lv_obj_t * cont, lv_obj_t * to_focus, lv_anim_enable_t anim_en)
93 {
94     LV_ASSERT_NULL(to_focus);
95 
96     uint32_t i;
97     uint32_t event_cnt = lv_obj_get_event_count(cont);
98     lv_gridnav_dsc_t * dsc = NULL;
99     for(i = 0; i < event_cnt; i++) {
100         lv_event_dsc_t * event_dsc = lv_obj_get_event_dsc(cont, i);
101         if(lv_event_dsc_get_cb(event_dsc) == gridnav_event_cb) {
102             dsc = lv_event_dsc_get_user_data(event_dsc);
103             break;
104         }
105     }
106 
107     if(dsc == NULL) {
108         LV_LOG_WARN("`cont` is not a gridnav container");
109         return;
110     }
111 
112     if(obj_is_focusable(to_focus) == false) {
113         LV_LOG_WARN("The object to focus is not focusable");
114         return;
115     }
116 
117     if(dsc->focused_obj) {
118         lv_obj_remove_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
119     }
120 
121     lv_obj_add_state(to_focus, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
122     lv_obj_scroll_to_view(to_focus, anim_en);
123     dsc->focused_obj = to_focus;
124 
125 }
126 
127 /**********************
128  *   STATIC FUNCTIONS
129  **********************/
130 
gridnav_event_cb(lv_event_t * e)131 static void gridnav_event_cb(lv_event_t * e)
132 {
133     lv_obj_t * obj = lv_event_get_current_target(e);
134     lv_gridnav_dsc_t * dsc = lv_event_get_user_data(e);
135     lv_event_code_t code = lv_event_get_code(e);
136 
137     if(code == LV_EVENT_KEY) {
138         uint32_t child_cnt = lv_obj_get_child_count(obj);
139         if(child_cnt == 0) return;
140 
141         if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj);
142         if(dsc->focused_obj == NULL) return;
143 
144         uint32_t key = lv_event_get_key(e);
145         lv_obj_t * guess = NULL;
146 
147         if(key == LV_KEY_RIGHT && !(dsc->ctrl & LV_GRIDNAV_CTRL_VERTICAL_MOVE_ONLY)) {
148             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
149                lv_obj_get_scroll_right(dsc->focused_obj) > 0) {
150                 int32_t d = lv_obj_get_width(dsc->focused_obj) / 4;
151                 if(d <= 0) d = 1;
152                 lv_obj_scroll_by_bounded(dsc->focused_obj, -d, 0, LV_ANIM_ON);
153             }
154             else {
155                 guess = find_chid(obj, dsc->focused_obj, FIND_RIGHT);
156                 if(guess == NULL) {
157                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
158                         guess = find_chid(obj, dsc->focused_obj, FIND_NEXT_ROW_FIRST_ITEM);
159                         if(guess == NULL) guess = find_first_focusable(obj);
160                     }
161                     else {
162                         lv_group_focus_next(lv_obj_get_group(obj));
163                     }
164                 }
165             }
166         }
167         else if(key == LV_KEY_LEFT && !(dsc->ctrl & LV_GRIDNAV_CTRL_VERTICAL_MOVE_ONLY)) {
168             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
169                lv_obj_get_scroll_left(dsc->focused_obj) > 0) {
170                 int32_t d = lv_obj_get_width(dsc->focused_obj) / 4;
171                 if(d <= 0) d = 1;
172                 lv_obj_scroll_by_bounded(dsc->focused_obj, d, 0, LV_ANIM_ON);
173             }
174             else {
175                 guess = find_chid(obj, dsc->focused_obj, FIND_LEFT);
176                 if(guess == NULL) {
177                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
178                         guess = find_chid(obj, dsc->focused_obj, FIND_PREV_ROW_LAST_ITEM);
179                         if(guess == NULL) guess = find_last_focusable(obj);
180                     }
181                     else {
182                         lv_group_focus_prev(lv_obj_get_group(obj));
183                     }
184                 }
185             }
186         }
187         else if(key == LV_KEY_DOWN && !(dsc->ctrl & LV_GRIDNAV_CTRL_HORIZONTAL_MOVE_ONLY)) {
188             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
189                lv_obj_get_scroll_bottom(dsc->focused_obj) > 0) {
190                 int32_t d = lv_obj_get_height(dsc->focused_obj) / 4;
191                 if(d <= 0) d = 1;
192                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, -d, LV_ANIM_ON);
193             }
194             else {
195                 guess = find_chid(obj, dsc->focused_obj, FIND_BOTTOM);
196                 if(guess == NULL) {
197                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
198                         guess = find_chid(obj, dsc->focused_obj, FIND_FIRST_ROW);
199                     }
200                     else {
201                         lv_group_focus_next(lv_obj_get_group(obj));
202                     }
203                 }
204             }
205         }
206         else if(key == LV_KEY_UP && !(dsc->ctrl & LV_GRIDNAV_CTRL_HORIZONTAL_MOVE_ONLY)) {
207             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
208                lv_obj_get_scroll_top(dsc->focused_obj) > 0) {
209                 int32_t d = lv_obj_get_height(dsc->focused_obj) / 4;
210                 if(d <= 0) d = 1;
211                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, d, LV_ANIM_ON);
212             }
213             else {
214                 guess = find_chid(obj, dsc->focused_obj, FIND_TOP);
215                 if(guess == NULL) {
216                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
217                         guess = find_chid(obj, dsc->focused_obj, FIND_LAST_ROW);
218                     }
219                     else {
220                         lv_group_focus_prev(lv_obj_get_group(obj));
221                     }
222                 }
223             }
224         }
225         else {
226             if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
227                 lv_obj_send_event(dsc->focused_obj, LV_EVENT_KEY, &key);
228             }
229         }
230 
231         if(guess && guess != dsc->focused_obj) {
232             lv_obj_remove_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
233             lv_obj_send_event(dsc->focused_obj, LV_EVENT_DEFOCUSED, lv_indev_active());
234             lv_obj_add_state(guess, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
235             lv_obj_send_event(guess, LV_EVENT_FOCUSED, lv_indev_active());
236             lv_obj_scroll_to_view(guess, LV_ANIM_ON);
237             dsc->focused_obj = guess;
238         }
239     }
240     else if(code == LV_EVENT_FOCUSED) {
241         if(dsc->focused_obj == NULL)  dsc->focused_obj = find_first_focusable(obj);
242         if(dsc->focused_obj) {
243             lv_obj_add_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
244             lv_obj_remove_state(dsc->focused_obj, LV_STATE_PRESSED); /*Be sure the focuses obj is not stuck in pressed state*/
245             lv_obj_scroll_to_view(dsc->focused_obj, LV_ANIM_OFF);
246         }
247     }
248     else if(code == LV_EVENT_DEFOCUSED) {
249         if(dsc->focused_obj) {
250             lv_obj_remove_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
251         }
252     }
253     else if(code == LV_EVENT_CHILD_CREATED) {
254         lv_obj_t * child = lv_event_get_target(e);
255         if(lv_obj_get_parent(child) == obj) {
256             if(dsc->focused_obj == NULL) {
257                 dsc->focused_obj = child;
258                 if(lv_obj_has_state(obj, LV_STATE_FOCUSED)) {
259                     lv_obj_add_state(child, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
260                     lv_obj_scroll_to_view(child, LV_ANIM_OFF);
261                 }
262             }
263         }
264     }
265     else if(code == LV_EVENT_CHILD_DELETED) {
266         /*This event bubble, so be sure this object's child was deleted.
267          *As we don't know which object was deleted we can't make the next focused.
268          *So make the first object focused*/
269         lv_obj_t * target = lv_event_get_target(e);
270         if(target == obj) {
271             dsc->focused_obj = find_first_focusable(obj);
272         }
273     }
274     else if(code == LV_EVENT_DELETE) {
275         lv_gridnav_remove(obj);
276     }
277     else if(code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_PRESS_LOST ||
278             code == LV_EVENT_SHORT_CLICKED || code == LV_EVENT_LONG_PRESSED || code == LV_EVENT_LONG_PRESSED_REPEAT ||
279             code == LV_EVENT_CLICKED || code == LV_EVENT_RELEASED) {
280         if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
281             /*Forward press/release related event too*/
282             lv_indev_type_t t = lv_indev_get_type(lv_indev_active());
283             if(t == LV_INDEV_TYPE_ENCODER || t == LV_INDEV_TYPE_KEYPAD) {
284                 lv_obj_send_event(dsc->focused_obj, code, lv_indev_active());
285             }
286         }
287     }
288 }
289 
find_chid(lv_obj_t * obj,lv_obj_t * start_child,find_mode_t mode)290 static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode)
291 {
292     int32_t x_start = get_x_center(start_child);
293     int32_t y_start = get_y_center(start_child);
294     uint32_t child_cnt = lv_obj_get_child_count(obj);
295     lv_obj_t * guess = NULL;
296     int32_t x_err_guess = LV_COORD_MAX;
297     int32_t y_err_guess = LV_COORD_MAX;
298     int32_t h_half = lv_obj_get_height(start_child) / 2;
299     int32_t h_max = lv_obj_get_height(obj) + lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
300     uint32_t i;
301     for(i = 0; i < child_cnt; i++) {
302         lv_obj_t * child = lv_obj_get_child(obj, i);
303         if(child == start_child) continue;
304         if(obj_is_focusable(child) == false) continue;
305 
306         int32_t x_err = 0;
307         int32_t y_err = 0;
308         switch(mode) {
309             case FIND_LEFT:
310                 x_err = get_x_center(child) - x_start;
311                 y_err = get_y_center(child) - y_start;
312                 if(x_err >= 0) continue;    /*It's on the right*/
313                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
314                 break;
315             case FIND_RIGHT:
316                 x_err = get_x_center(child) - x_start;
317                 y_err = get_y_center(child) - y_start;
318                 if(x_err <= 0) continue;    /*It's on the left*/
319                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
320                 break;
321             case FIND_TOP:
322                 x_err = get_x_center(child) - x_start;
323                 y_err = get_y_center(child) - y_start;
324                 if(y_err >= 0) continue;    /*It's on the bottom*/
325                 break;
326             case FIND_BOTTOM:
327                 x_err = get_x_center(child) - x_start;
328                 y_err = get_y_center(child) - y_start;
329                 if(y_err <= 0) continue;    /*It's on the top*/
330                 break;
331             case FIND_NEXT_ROW_FIRST_ITEM:
332                 y_err = get_y_center(child) - y_start;
333                 if(y_err <= 0) continue;    /*It's on the top*/
334                 x_err = lv_obj_get_x(child);
335                 break;
336             case FIND_PREV_ROW_LAST_ITEM:
337                 y_err = get_y_center(child) - y_start;
338                 if(y_err >= 0) continue;    /*It's on the bottom*/
339                 x_err = obj->coords.x2 - child->coords.x2;
340                 break;
341             case FIND_FIRST_ROW:
342                 x_err = get_x_center(child) - x_start;
343                 y_err = lv_obj_get_y(child);
344                 break;
345             case FIND_LAST_ROW:
346                 x_err = get_x_center(child) - x_start;
347                 y_err = h_max - lv_obj_get_y(child);
348         }
349 
350         if(guess == NULL ||
351            (y_err * y_err + x_err * x_err < y_err_guess * y_err_guess + x_err_guess * x_err_guess)) {
352             guess = child;
353             x_err_guess  = x_err;
354             y_err_guess  = y_err;
355         }
356     }
357     return guess;
358 }
359 
find_first_focusable(lv_obj_t * obj)360 static lv_obj_t * find_first_focusable(lv_obj_t * obj)
361 {
362     uint32_t child_cnt = lv_obj_get_child_count(obj);
363     uint32_t i;
364     for(i = 0; i < child_cnt; i++) {
365         lv_obj_t * child = lv_obj_get_child(obj, i);
366         if(obj_is_focusable(child)) return child;
367 
368     }
369     return NULL;
370 }
371 
find_last_focusable(lv_obj_t * obj)372 static lv_obj_t * find_last_focusable(lv_obj_t * obj)
373 {
374     uint32_t child_cnt = lv_obj_get_child_count(obj);
375     int32_t i;
376     for(i = child_cnt - 1; i >= 0; i--) {
377         lv_obj_t * child = lv_obj_get_child(obj, i);
378         if(obj_is_focusable(child)) return child;
379     }
380     return NULL;
381 }
382 
obj_is_focusable(lv_obj_t * obj)383 static bool obj_is_focusable(lv_obj_t * obj)
384 {
385     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return false;
386     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)) return true;
387     else return false;
388 }
389 
get_x_center(lv_obj_t * obj)390 static int32_t get_x_center(lv_obj_t * obj)
391 {
392     return obj->coords.x1 + lv_area_get_width(&obj->coords) / 2;
393 }
394 
get_y_center(lv_obj_t * obj)395 static int32_t get_y_center(lv_obj_t * obj)
396 {
397     return obj->coords.y1 + lv_area_get_height(&obj->coords) / 2;
398 }
399 
400 #endif /*LV_USE_GRIDNAV*/
401