1 | /* GTK - The GIMP Toolkit |
2 | * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | */ |
17 | |
18 | #include "gtkwidgetprivate.h" |
19 | #include "gtknative.h" |
20 | |
21 | typedef struct _CompareInfo CompareInfo; |
22 | |
23 | enum Axis { |
24 | HORIZONTAL = 0, |
25 | VERTICAL = 1 |
26 | }; |
27 | |
28 | struct _CompareInfo |
29 | { |
30 | GtkWidget *widget; |
31 | int x; |
32 | int y; |
33 | guint reverse : 1; |
34 | guint axis : 1; |
35 | }; |
36 | |
37 | static inline void |
38 | get_axis_info (const graphene_rect_t *bounds, |
39 | int axis, |
40 | int *start, |
41 | int *end) |
42 | { |
43 | if (axis == HORIZONTAL) |
44 | { |
45 | *start = bounds->origin.x; |
46 | *end = bounds->size.width; |
47 | } |
48 | else if (axis == VERTICAL) |
49 | { |
50 | *start = bounds->origin.y; |
51 | *end = bounds->size.height; |
52 | } |
53 | else |
54 | g_assert(FALSE); |
55 | } |
56 | |
57 | /* Utility function, equivalent to g_list_reverse */ |
58 | static void |
59 | reverse_ptr_array (GPtrArray *arr) |
60 | { |
61 | int i; |
62 | |
63 | for (i = 0; i < arr->len / 2; i ++) |
64 | { |
65 | void *a = g_ptr_array_index (arr, i); |
66 | void *b = g_ptr_array_index (arr, arr->len - 1 - i); |
67 | |
68 | arr->pdata[i] = b; |
69 | arr->pdata[arr->len - 1 - i] = a; |
70 | } |
71 | } |
72 | |
73 | static int |
74 | tab_sort_func (gconstpointer a, |
75 | gconstpointer b, |
76 | gpointer user_data) |
77 | { |
78 | graphene_rect_t child_bounds1, child_bounds2; |
79 | GtkWidget *child1 = *((GtkWidget **)a); |
80 | GtkWidget *child2 = *((GtkWidget **)b); |
81 | GtkTextDirection text_direction = GPOINTER_TO_INT (user_data); |
82 | float y1, y2; |
83 | |
84 | if (!gtk_widget_compute_bounds (widget: child1, target: _gtk_widget_get_parent (widget: child1), out_bounds: &child_bounds1) || |
85 | !gtk_widget_compute_bounds (widget: child2, target: _gtk_widget_get_parent (widget: child2), out_bounds: &child_bounds2)) |
86 | return 0; |
87 | |
88 | y1 = child_bounds1.origin.y + (child_bounds1.size.height / 2.0f); |
89 | y2 = child_bounds2.origin.y + (child_bounds2.size.height / 2.0f); |
90 | |
91 | if (y1 == y2) |
92 | { |
93 | const float x1 = child_bounds1.origin.x + (child_bounds1.size.width / 2.0f); |
94 | const float x2 = child_bounds2.origin.x + (child_bounds2.size.width / 2.0f); |
95 | |
96 | if (text_direction == GTK_TEXT_DIR_RTL) |
97 | return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1); |
98 | else |
99 | return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1); |
100 | } |
101 | else |
102 | return (y1 < y2) ? -1 : 1; |
103 | } |
104 | |
105 | static void |
106 | focus_sort_tab (GtkWidget *widget, |
107 | GtkDirectionType direction, |
108 | GPtrArray *focus_order) |
109 | { |
110 | GtkTextDirection text_direction = _gtk_widget_get_direction (widget); |
111 | |
112 | g_ptr_array_sort_with_data (array: focus_order, compare_func: tab_sort_func, GINT_TO_POINTER (text_direction)); |
113 | |
114 | if (direction == GTK_DIR_TAB_BACKWARD) |
115 | reverse_ptr_array (arr: focus_order); |
116 | } |
117 | |
118 | /* Look for a child in @children that is intermediate between |
119 | * the focus widget and container. This widget, if it exists, |
120 | * acts as the starting widget for focus navigation. |
121 | */ |
122 | static GtkWidget * |
123 | find_old_focus (GtkWidget *widget, |
124 | GPtrArray *children) |
125 | { |
126 | int i; |
127 | |
128 | for (i = 0; i < children->len; i ++) |
129 | { |
130 | GtkWidget *child = g_ptr_array_index (children, i); |
131 | GtkWidget *child_ptr = child; |
132 | |
133 | while (child_ptr && child_ptr != widget) |
134 | { |
135 | GtkWidget *parent; |
136 | |
137 | parent = _gtk_widget_get_parent (widget: child_ptr); |
138 | |
139 | if (parent && (_gtk_widget_get_focus_child (widget: parent) != child_ptr)) |
140 | { |
141 | child = NULL; |
142 | break; |
143 | } |
144 | |
145 | child_ptr = parent; |
146 | } |
147 | |
148 | if (child) |
149 | return child; |
150 | } |
151 | |
152 | return NULL; |
153 | } |
154 | |
155 | static gboolean |
156 | old_focus_coords (GtkWidget *widget, |
157 | graphene_rect_t *old_focus_bounds) |
158 | { |
159 | GtkWidget *old_focus; |
160 | |
161 | old_focus = gtk_root_get_focus (self: gtk_widget_get_root (widget)); |
162 | if (old_focus) |
163 | return gtk_widget_compute_bounds (widget: old_focus, target: widget, out_bounds: old_focus_bounds); |
164 | |
165 | return FALSE; |
166 | } |
167 | |
168 | static int |
169 | axis_compare (gconstpointer a, |
170 | gconstpointer b, |
171 | gpointer user_data) |
172 | { |
173 | graphene_rect_t bounds1; |
174 | graphene_rect_t bounds2; |
175 | CompareInfo *compare = user_data; |
176 | int start1, end1; |
177 | int start2, end2; |
178 | |
179 | if (!gtk_widget_compute_bounds (widget: *((GtkWidget **)a), target: compare->widget, out_bounds: &bounds1) || |
180 | !gtk_widget_compute_bounds (widget: *((GtkWidget **)b), target: compare->widget, out_bounds: &bounds2)) |
181 | return 0; |
182 | |
183 | get_axis_info (bounds: &bounds1, axis: compare->axis, start: &start1, end: &end1); |
184 | get_axis_info (bounds: &bounds2, axis: compare->axis, start: &start2, end: &end2); |
185 | |
186 | start1 = start1 + (end1 / 2); |
187 | start2 = start2 + (end2 / 2); |
188 | |
189 | if (start1 == start2) |
190 | { |
191 | /* Now use origin/bounds to compare the 2 widgets on the other axis */ |
192 | get_axis_info (bounds: &bounds1, axis: 1 - compare->axis, start: &start1, end: &end1); |
193 | get_axis_info (bounds: &bounds2, axis: 1 - compare->axis, start: &start2, end: &end2); |
194 | |
195 | int x1 = abs (x: start1 + (end1 / 2) - compare->x); |
196 | int x2 = abs (x: start2 + (end2 / 2) - compare->x); |
197 | |
198 | if (compare->reverse) |
199 | return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1); |
200 | else |
201 | return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1); |
202 | } |
203 | else |
204 | return (start1 < start2) ? -1 : 1; |
205 | } |
206 | |
207 | static void |
208 | focus_sort_left_right (GtkWidget *widget, |
209 | GtkDirectionType direction, |
210 | GPtrArray *focus_order) |
211 | { |
212 | CompareInfo compare_info; |
213 | GtkWidget *old_focus = _gtk_widget_get_focus_child (widget); |
214 | graphene_rect_t old_bounds; |
215 | |
216 | compare_info.widget = widget; |
217 | compare_info.reverse = (direction == GTK_DIR_LEFT); |
218 | |
219 | if (!old_focus) |
220 | old_focus = find_old_focus (widget, children: focus_order); |
221 | |
222 | if (old_focus && gtk_widget_compute_bounds (widget: old_focus, target: widget, out_bounds: &old_bounds)) |
223 | { |
224 | float compare_y1; |
225 | float compare_y2; |
226 | float compare_x; |
227 | int i; |
228 | |
229 | /* Delete widgets from list that don't match minimum criteria */ |
230 | |
231 | compare_y1 = old_bounds.origin.y; |
232 | compare_y2 = old_bounds.origin.y + old_bounds.size.height; |
233 | |
234 | if (direction == GTK_DIR_LEFT) |
235 | compare_x = old_bounds.origin.x; |
236 | else |
237 | compare_x = old_bounds.origin.x + old_bounds.size.width; |
238 | |
239 | for (i = 0; i < focus_order->len; i ++) |
240 | { |
241 | GtkWidget *child = g_ptr_array_index (focus_order, i); |
242 | |
243 | if (child != old_focus) |
244 | { |
245 | graphene_rect_t child_bounds; |
246 | |
247 | if (gtk_widget_compute_bounds (widget: child, target: widget, out_bounds: &child_bounds)) |
248 | { |
249 | const float child_y1 = child_bounds.origin.y; |
250 | const float child_y2 = child_bounds.origin.y + child_bounds.size.height; |
251 | |
252 | if ((child_y2 <= compare_y1 || child_y1 >= compare_y2) /* No vertical overlap */ || |
253 | (direction == GTK_DIR_RIGHT && child_bounds.origin.x + child_bounds.size.width < compare_x) || /* Not to left */ |
254 | (direction == GTK_DIR_LEFT && child_bounds.origin.x > compare_x)) /* Not to right */ |
255 | { |
256 | g_ptr_array_remove_index (array: focus_order, index_: i); |
257 | i --; |
258 | } |
259 | } |
260 | else |
261 | { |
262 | g_ptr_array_remove_index (array: focus_order, index_: i); |
263 | i --; |
264 | } |
265 | } |
266 | } |
267 | |
268 | compare_info.y = (compare_y1 + compare_y2) / 2; |
269 | compare_info.x = old_bounds.origin.x + (old_bounds.size.width / 2.0f); |
270 | } |
271 | else |
272 | { |
273 | /* No old focus widget, need to figure out starting x,y some other way |
274 | */ |
275 | graphene_rect_t bounds; |
276 | GtkWidget *parent; |
277 | graphene_rect_t old_focus_bounds; |
278 | |
279 | parent = gtk_widget_get_parent (widget); |
280 | if (!gtk_widget_compute_bounds (widget, target: parent ? parent : widget, out_bounds: &bounds)) |
281 | graphene_rect_init (r: &bounds, x: 0, y: 0, width: 0, height: 0); |
282 | |
283 | if (old_focus_coords (widget, old_focus_bounds: &old_focus_bounds)) |
284 | { |
285 | compare_info.y = old_focus_bounds.origin.y + (old_focus_bounds.size.height / 2.0f); |
286 | } |
287 | else |
288 | { |
289 | if (!GTK_IS_NATIVE (ptr: widget)) |
290 | compare_info.y = bounds.origin.y + bounds.size.height; |
291 | else |
292 | compare_info.y = bounds.size.height / 2.0f; |
293 | } |
294 | |
295 | if (!GTK_IS_NATIVE (ptr: widget)) |
296 | compare_info.x = (direction == GTK_DIR_RIGHT) ? bounds.origin.x : bounds.origin.x + bounds.size.width; |
297 | else |
298 | compare_info.x = (direction == GTK_DIR_RIGHT) ? 0 : bounds.size.width; |
299 | } |
300 | |
301 | |
302 | compare_info.axis = HORIZONTAL; |
303 | g_ptr_array_sort_with_data (array: focus_order, compare_func: axis_compare, user_data: &compare_info); |
304 | |
305 | if (compare_info.reverse) |
306 | reverse_ptr_array (arr: focus_order); |
307 | } |
308 | |
309 | static void |
310 | focus_sort_up_down (GtkWidget *widget, |
311 | GtkDirectionType direction, |
312 | GPtrArray *focus_order) |
313 | { |
314 | CompareInfo compare_info; |
315 | GtkWidget *old_focus = _gtk_widget_get_focus_child (widget); |
316 | graphene_rect_t old_bounds; |
317 | |
318 | compare_info.widget = widget; |
319 | compare_info.reverse = (direction == GTK_DIR_UP); |
320 | |
321 | if (!old_focus) |
322 | old_focus = find_old_focus (widget, children: focus_order); |
323 | |
324 | if (old_focus && gtk_widget_compute_bounds (widget: old_focus, target: widget, out_bounds: &old_bounds)) |
325 | { |
326 | float compare_x1; |
327 | float compare_x2; |
328 | float compare_y; |
329 | int i; |
330 | |
331 | /* Delete widgets from list that don't match minimum criteria */ |
332 | |
333 | compare_x1 = old_bounds.origin.x; |
334 | compare_x2 = old_bounds.origin.x + old_bounds.size.width; |
335 | |
336 | if (direction == GTK_DIR_UP) |
337 | compare_y = old_bounds.origin.y; |
338 | else |
339 | compare_y = old_bounds.origin.y + old_bounds.size.height; |
340 | |
341 | for (i = 0; i < focus_order->len; i ++) |
342 | { |
343 | GtkWidget *child = g_ptr_array_index (focus_order, i); |
344 | |
345 | if (child != old_focus) |
346 | { |
347 | graphene_rect_t child_bounds; |
348 | |
349 | if (gtk_widget_compute_bounds (widget: child, target: widget, out_bounds: &child_bounds)) |
350 | { |
351 | const float child_x1 = child_bounds.origin.x; |
352 | const float child_x2 = child_bounds.origin.x + child_bounds.size.width; |
353 | |
354 | if ((child_x2 <= compare_x1 || child_x1 >= compare_x2) /* No horizontal overlap */ || |
355 | (direction == GTK_DIR_DOWN && child_bounds.origin.y + child_bounds.size.height < compare_y) || /* Not below */ |
356 | (direction == GTK_DIR_UP && child_bounds.origin.y > compare_y)) /* Not above */ |
357 | { |
358 | g_ptr_array_remove_index (array: focus_order, index_: i); |
359 | i --; |
360 | } |
361 | } |
362 | else |
363 | { |
364 | g_ptr_array_remove_index (array: focus_order, index_: i); |
365 | i --; |
366 | } |
367 | } |
368 | } |
369 | |
370 | compare_info.x = (compare_x1 + compare_x2) / 2; |
371 | compare_info.y = old_bounds.origin.y + (old_bounds.size.height / 2.0f); |
372 | } |
373 | else |
374 | { |
375 | /* No old focus widget, need to figure out starting x,y some other way |
376 | */ |
377 | GtkWidget *parent; |
378 | graphene_rect_t bounds; |
379 | graphene_rect_t old_focus_bounds; |
380 | |
381 | parent = gtk_widget_get_parent (widget); |
382 | if (!gtk_widget_compute_bounds (widget, target: parent ? parent : widget, out_bounds: &bounds)) |
383 | graphene_rect_init (r: &bounds, x: 0, y: 0, width: 0, height: 0); |
384 | |
385 | if (old_focus_coords (widget, old_focus_bounds: &old_focus_bounds)) |
386 | { |
387 | compare_info.x = old_focus_bounds.origin.x + (old_focus_bounds.size.width / 2.0f); |
388 | } |
389 | else |
390 | { |
391 | if (!GTK_IS_NATIVE (ptr: widget)) |
392 | compare_info.x = bounds.origin.x + (bounds.size.width / 2.0f); |
393 | else |
394 | compare_info.x = bounds.size.width / 2.0f; |
395 | } |
396 | |
397 | if (!GTK_IS_NATIVE (ptr: widget)) |
398 | compare_info.y = (direction == GTK_DIR_DOWN) ? bounds.origin.y : bounds.origin.y + bounds.size.height; |
399 | else |
400 | compare_info.y = (direction == GTK_DIR_DOWN) ? 0 : + bounds.size.height; |
401 | } |
402 | |
403 | compare_info.axis = VERTICAL; |
404 | g_ptr_array_sort_with_data (array: focus_order, compare_func: axis_compare, user_data: &compare_info); |
405 | |
406 | if (compare_info.reverse) |
407 | reverse_ptr_array (arr: focus_order); |
408 | } |
409 | |
410 | void |
411 | gtk_widget_focus_sort (GtkWidget *widget, |
412 | GtkDirectionType direction, |
413 | GPtrArray *focus_order) |
414 | { |
415 | GtkWidget *child; |
416 | |
417 | g_assert (focus_order != NULL); |
418 | |
419 | if (focus_order->len == 0) |
420 | { |
421 | /* Initialize the list with all visible child widgets */ |
422 | for (child = _gtk_widget_get_first_child (widget); |
423 | child != NULL; |
424 | child = _gtk_widget_get_next_sibling (widget: child)) |
425 | { |
426 | if (_gtk_widget_get_mapped (widget: child) && |
427 | gtk_widget_get_sensitive (widget: child)) |
428 | g_ptr_array_add (array: focus_order, data: child); |
429 | } |
430 | } |
431 | |
432 | /* Now sort that list depending on @direction */ |
433 | switch (direction) |
434 | { |
435 | case GTK_DIR_TAB_FORWARD: |
436 | case GTK_DIR_TAB_BACKWARD: |
437 | focus_sort_tab (widget, direction, focus_order); |
438 | break; |
439 | case GTK_DIR_UP: |
440 | case GTK_DIR_DOWN: |
441 | focus_sort_up_down (widget, direction, focus_order); |
442 | break; |
443 | case GTK_DIR_LEFT: |
444 | case GTK_DIR_RIGHT: |
445 | focus_sort_left_right (widget, direction, focus_order); |
446 | break; |
447 | default: |
448 | g_assert_not_reached (); |
449 | } |
450 | } |
451 | |
452 | |
453 | gboolean |
454 | gtk_widget_focus_move (GtkWidget *widget, |
455 | GtkDirectionType direction) |
456 | { |
457 | GPtrArray *focus_order; |
458 | GtkWidget *focus_child = _gtk_widget_get_focus_child (widget); |
459 | int i; |
460 | gboolean ret = FALSE; |
461 | |
462 | focus_order = g_ptr_array_new (); |
463 | gtk_widget_focus_sort (widget, direction, focus_order); |
464 | |
465 | for (i = 0; i < focus_order->len && !ret; i++) |
466 | { |
467 | GtkWidget *child = g_ptr_array_index (focus_order, i); |
468 | |
469 | if (focus_child) |
470 | { |
471 | if (focus_child == child) |
472 | { |
473 | focus_child = NULL; |
474 | ret = gtk_widget_child_focus (widget: child, direction); |
475 | } |
476 | } |
477 | else if (_gtk_widget_get_mapped (widget: child) && |
478 | gtk_widget_is_ancestor (widget: child, ancestor: widget)) |
479 | { |
480 | ret = gtk_widget_child_focus (widget: child, direction); |
481 | } |
482 | } |
483 | |
484 | g_ptr_array_unref (array: focus_order); |
485 | |
486 | return ret; |
487 | } |
488 | |