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 "adw-widget-utils-private.h"
19 
20 typedef struct _CompareInfo CompareInfo;
21 
22 enum Axis {
23   HORIZONTAL = 0,
24   VERTICAL   = 1
25 };
26 
27 struct _CompareInfo
28 {
29   GtkWidget *widget;
30   int x;
31   int y;
32   guint reverse : 1;
33   guint axis : 1;
34 };
35 
36 static inline void
get_axis_info(const graphene_rect_t * bounds,int axis,int * start,int * end)37 get_axis_info (const graphene_rect_t *bounds,
38                int                    axis,
39                int                   *start,
40                int                   *end)
41 {
42   if (axis == HORIZONTAL) {
43     *start = bounds->origin.x;
44     *end = bounds->size.width;
45   } else if (axis == VERTICAL) {
46     *start = bounds->origin.y;
47     *end = bounds->size.height;
48   } else {
49     g_assert_not_reached ();
50   }
51 }
52 
53 /* Utility function, equivalent to g_list_reverse */
54 static void
reverse_ptr_array(GPtrArray * arr)55 reverse_ptr_array (GPtrArray *arr)
56 {
57   int i;
58 
59   for (i = 0; i < arr->len / 2; i ++) {
60     void *a = g_ptr_array_index (arr, i);
61     void *b = g_ptr_array_index (arr, arr->len - 1 - i);
62 
63     arr->pdata[i] = b;
64     arr->pdata[arr->len - 1 - i] = a;
65   }
66 }
67 
68 static int
tab_sort_func(gconstpointer a,gconstpointer b,gpointer user_data)69 tab_sort_func (gconstpointer a,
70                gconstpointer b,
71                gpointer      user_data)
72 {
73   graphene_rect_t child_bounds1, child_bounds2;
74   GtkWidget *child1 = *((GtkWidget **) a);
75   GtkWidget *child2 = *((GtkWidget **) b);
76   GtkTextDirection text_direction = GPOINTER_TO_INT (user_data);
77   float y1, y2;
78 
79   if (!gtk_widget_compute_bounds (child1, gtk_widget_get_parent (child1), &child_bounds1) ||
80       !gtk_widget_compute_bounds (child2, gtk_widget_get_parent (child2), &child_bounds2))
81     return 0;
82 
83   y1 = child_bounds1.origin.y + (child_bounds1.size.height / 2.0f);
84   y2 = child_bounds2.origin.y + (child_bounds2.size.height / 2.0f);
85 
86   if (y1 == y2) {
87     const float x1 = child_bounds1.origin.x + (child_bounds1.size.width / 2.0f);
88     const float x2 = child_bounds2.origin.x + (child_bounds2.size.width / 2.0f);
89 
90     if (text_direction == GTK_TEXT_DIR_RTL)
91       return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1);
92     else
93       return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1);
94   }
95 
96   return (y1 < y2) ? -1 : 1;
97 }
98 
99 static void
focus_sort_tab(GtkWidget * widget,GtkDirectionType direction,GPtrArray * focus_order)100 focus_sort_tab (GtkWidget        *widget,
101                 GtkDirectionType  direction,
102                 GPtrArray        *focus_order)
103 {
104   GtkTextDirection text_direction = gtk_widget_get_direction (widget);
105 
106   g_ptr_array_sort_with_data (focus_order, tab_sort_func, GINT_TO_POINTER (text_direction));
107 
108   if (direction == GTK_DIR_TAB_BACKWARD)
109     reverse_ptr_array (focus_order);
110 }
111 
112 /* Look for a child in @children that is intermediate between
113  * the focus widget and container. This widget, if it exists,
114  * acts as the starting widget for focus navigation.
115  */
116 static GtkWidget *
find_old_focus(GtkWidget * widget,GPtrArray * children)117 find_old_focus (GtkWidget *widget,
118                 GPtrArray *children)
119 {
120   int i;
121 
122   for (i = 0; i < children->len; i ++) {
123     GtkWidget *child = g_ptr_array_index (children, i);
124     GtkWidget *child_ptr = child;
125 
126     while (child_ptr && child_ptr != widget) {
127       GtkWidget *parent = gtk_widget_get_parent (child_ptr);
128 
129       if (parent && (gtk_widget_get_focus_child (parent) != child_ptr)) {
130         child = NULL;
131         break;
132       }
133 
134       child_ptr = parent;
135     }
136 
137     if (child)
138       return child;
139   }
140 
141   return NULL;
142 }
143 
144 static gboolean
old_focus_coords(GtkWidget * widget,graphene_rect_t * old_focus_bounds)145 old_focus_coords (GtkWidget       *widget,
146                   graphene_rect_t *old_focus_bounds)
147 {
148   GtkWidget *old_focus;
149 
150   old_focus = gtk_root_get_focus (gtk_widget_get_root (widget));
151   if (old_focus)
152     return gtk_widget_compute_bounds (old_focus, widget, old_focus_bounds);
153 
154   return FALSE;
155 }
156 
157 static int
axis_compare(gconstpointer a,gconstpointer b,gpointer user_data)158 axis_compare (gconstpointer a,
159               gconstpointer b,
160               gpointer      user_data)
161 {
162   graphene_rect_t bounds1;
163   graphene_rect_t bounds2;
164   CompareInfo *compare = user_data;
165   int start1, end1;
166   int start2, end2;
167 
168   if (!gtk_widget_compute_bounds (*((GtkWidget **) a), compare->widget, &bounds1) ||
169       !gtk_widget_compute_bounds (*((GtkWidget **) b), compare->widget, &bounds2))
170     return 0;
171 
172   get_axis_info (&bounds1, compare->axis, &start1, &end1);
173   get_axis_info (&bounds2, compare->axis, &start2, &end2);
174 
175   start1 = start1 + (end1 / 2);
176   start2 = start2 + (end2 / 2);
177 
178   if (start1 == start2) {
179     int x1, x2;
180 
181     /* Now use origin/bounds to compare the 2 widgets on the other axis */
182     get_axis_info (&bounds1, 1 - compare->axis, &start1, &end1);
183     get_axis_info (&bounds2, 1 - compare->axis, &start2, &end2);
184 
185     x1 = abs (start1 + (end1 / 2) - compare->x);
186     x2 = abs (start2 + (end2 / 2) - compare->x);
187 
188     if (compare->reverse)
189       return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1);
190     else
191       return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1);
192   }
193 
194   return (start1 < start2) ? -1 : 1;
195 }
196 
197 static void
focus_sort_left_right(GtkWidget * widget,GtkDirectionType direction,GPtrArray * focus_order)198 focus_sort_left_right (GtkWidget        *widget,
199                        GtkDirectionType  direction,
200                        GPtrArray        *focus_order)
201 {
202   CompareInfo compare_info;
203   GtkWidget *old_focus = gtk_widget_get_focus_child (widget);
204   graphene_rect_t old_bounds;
205 
206   compare_info.widget = widget;
207   compare_info.reverse = (direction == GTK_DIR_LEFT);
208 
209   if (!old_focus)
210     old_focus = find_old_focus (widget, focus_order);
211 
212   if (old_focus && gtk_widget_compute_bounds (old_focus, widget, &old_bounds)) {
213     float compare_y1;
214     float compare_y2;
215     float compare_x;
216     int i;
217 
218     /* Delete widgets from list that don't match minimum criteria */
219 
220     compare_y1 = old_bounds.origin.y;
221     compare_y2 = old_bounds.origin.y + old_bounds.size.height;
222 
223     if (direction == GTK_DIR_LEFT)
224       compare_x = old_bounds.origin.x;
225     else
226       compare_x = old_bounds.origin.x + old_bounds.size.width;
227 
228     for (i = 0; i < focus_order->len; i++) {
229       GtkWidget *child = g_ptr_array_index (focus_order, i);
230 
231       if (child != old_focus) {
232         graphene_rect_t child_bounds;
233 
234         if (gtk_widget_compute_bounds (child, widget, &child_bounds)) {
235           const float child_y1 = child_bounds.origin.y;
236           const float child_y2 = child_bounds.origin.y + child_bounds.size.height;
237 
238           if ((child_y2 <= compare_y1 || child_y1 >= compare_y2) /* No vertical overlap */ ||
239               (direction == GTK_DIR_RIGHT && child_bounds.origin.x + child_bounds.size.width < compare_x) || /* Not to left */
240               (direction == GTK_DIR_LEFT && child_bounds.origin.x > compare_x)) /* Not to right */ {
241             g_ptr_array_remove_index (focus_order, i);
242             i --;
243           }
244         } else {
245           g_ptr_array_remove_index (focus_order, i);
246           i --;
247         }
248       }
249     }
250 
251     compare_info.y = (compare_y1 + compare_y2) / 2;
252     compare_info.x = old_bounds.origin.x + (old_bounds.size.width / 2.0f);
253   } else {
254     /* No old focus widget, need to figure out starting x,y some other way
255      */
256     graphene_rect_t bounds;
257     GtkWidget *parent;
258     graphene_rect_t old_focus_bounds;
259 
260     parent = gtk_widget_get_parent (widget);
261     if (!gtk_widget_compute_bounds (widget, parent ? parent : widget, &bounds))
262       graphene_rect_init (&bounds, 0, 0, 0, 0);
263 
264     if (old_focus_coords (widget, &old_focus_bounds)) {
265       compare_info.y = old_focus_bounds.origin.y + (old_focus_bounds.size.height / 2.0f);
266     } else {
267       if (!GTK_IS_NATIVE (widget))
268         compare_info.y = bounds.origin.y + bounds.size.height;
269       else
270         compare_info.y = bounds.size.height / 2.0f;
271     }
272 
273     if (!GTK_IS_NATIVE (widget))
274       compare_info.x = (direction == GTK_DIR_RIGHT) ? bounds.origin.x : bounds.origin.x + bounds.size.width;
275     else
276       compare_info.x = (direction == GTK_DIR_RIGHT) ? 0 : bounds.size.width;
277   }
278 
279   compare_info.axis = HORIZONTAL;
280   g_ptr_array_sort_with_data (focus_order, axis_compare, &compare_info);
281 
282   if (compare_info.reverse)
283     reverse_ptr_array (focus_order);
284 }
285 
286 static void
focus_sort_up_down(GtkWidget * widget,GtkDirectionType direction,GPtrArray * focus_order)287 focus_sort_up_down (GtkWidget        *widget,
288                     GtkDirectionType  direction,
289                     GPtrArray        *focus_order)
290 {
291   CompareInfo compare_info;
292   GtkWidget *old_focus = gtk_widget_get_focus_child (widget);
293   graphene_rect_t old_bounds;
294 
295   compare_info.widget = widget;
296   compare_info.reverse = (direction == GTK_DIR_UP);
297 
298   if (!old_focus)
299     old_focus = find_old_focus (widget, focus_order);
300 
301   if (old_focus && gtk_widget_compute_bounds (old_focus, widget, &old_bounds)) {
302     float compare_x1;
303     float compare_x2;
304     float compare_y;
305     int i;
306 
307     /* Delete widgets from list that don't match minimum criteria */
308 
309     compare_x1 = old_bounds.origin.x;
310     compare_x2 = old_bounds.origin.x + old_bounds.size.width;
311 
312     if (direction == GTK_DIR_UP)
313       compare_y = old_bounds.origin.y;
314     else
315       compare_y = old_bounds.origin.y + old_bounds.size.height;
316 
317     for (i = 0; i < focus_order->len; i++) {
318       GtkWidget *child = g_ptr_array_index (focus_order, i);
319 
320       if (child != old_focus) {
321         graphene_rect_t child_bounds;
322 
323         if (gtk_widget_compute_bounds (child, widget, &child_bounds)) {
324           const float child_x1 = child_bounds.origin.x;
325           const float child_x2 = child_bounds.origin.x + child_bounds.size.width;
326 
327           if ((child_x2 <= compare_x1 || child_x1 >= compare_x2) /* No horizontal overlap */ ||
328               (direction == GTK_DIR_DOWN && child_bounds.origin.y + child_bounds.size.height < compare_y) || /* Not below */
329               (direction == GTK_DIR_UP && child_bounds.origin.y > compare_y)) /* Not above */ {
330             g_ptr_array_remove_index (focus_order, i);
331             i --;
332           }
333         } else {
334           g_ptr_array_remove_index (focus_order, i);
335           i --;
336         }
337       }
338     }
339 
340     compare_info.x = (compare_x1 + compare_x2) / 2;
341     compare_info.y = old_bounds.origin.y + (old_bounds.size.height / 2.0f);
342   } else {
343     /* No old focus widget, need to figure out starting x,y some other way
344      */
345     GtkWidget *parent;
346     graphene_rect_t bounds;
347     graphene_rect_t old_focus_bounds;
348 
349     parent = gtk_widget_get_parent (widget);
350     if (!gtk_widget_compute_bounds (widget, parent ? parent : widget, &bounds))
351       graphene_rect_init (&bounds, 0, 0, 0, 0);
352 
353     if (old_focus_coords (widget, &old_focus_bounds)) {
354       compare_info.x = old_focus_bounds.origin.x + (old_focus_bounds.size.width / 2.0f);
355     } else {
356       if (!GTK_IS_NATIVE (widget))
357         compare_info.x = bounds.origin.x + (bounds.size.width / 2.0f);
358       else
359         compare_info.x = bounds.size.width / 2.0f;
360     }
361 
362     if (!GTK_IS_NATIVE (widget))
363       compare_info.y = (direction == GTK_DIR_DOWN) ? bounds.origin.y : bounds.origin.y + bounds.size.height;
364     else
365       compare_info.y = (direction == GTK_DIR_DOWN) ? 0 : + bounds.size.height;
366   }
367 
368   compare_info.axis = VERTICAL;
369   g_ptr_array_sort_with_data (focus_order, axis_compare, &compare_info);
370 
371   if (compare_info.reverse)
372     reverse_ptr_array (focus_order);
373 }
374 
375 static void
focus_sort(GtkWidget * widget,GtkDirectionType direction,GPtrArray * focus_order)376 focus_sort (GtkWidget        *widget,
377             GtkDirectionType  direction,
378             GPtrArray        *focus_order)
379 {
380   GtkWidget *child;
381 
382   g_assert (focus_order != NULL);
383 
384   if (focus_order->len == 0) {
385     /* Initialize the list with all visible child widgets */
386     for (child = gtk_widget_get_first_child (widget);
387          child != NULL;
388          child = gtk_widget_get_next_sibling (child)) {
389       if (gtk_widget_get_mapped (child) &&
390           gtk_widget_get_sensitive (child))
391         g_ptr_array_add (focus_order, child);
392     }
393   }
394 
395   /* Now sort that list depending on @direction */
396   switch (direction) {
397   case GTK_DIR_TAB_FORWARD:
398   case GTK_DIR_TAB_BACKWARD:
399     focus_sort_tab (widget, direction, focus_order);
400     break;
401   case GTK_DIR_UP:
402   case GTK_DIR_DOWN:
403     focus_sort_up_down (widget, direction, focus_order);
404     break;
405   case GTK_DIR_LEFT:
406   case GTK_DIR_RIGHT:
407     focus_sort_left_right (widget, direction, focus_order);
408     break;
409   default:
410     g_assert_not_reached ();
411   }
412 }
413 
414 
415 static gboolean
focus_move(GtkWidget * widget,GtkDirectionType direction)416 focus_move (GtkWidget        *widget,
417             GtkDirectionType  direction)
418 {
419   GPtrArray *focus_order;
420   GtkWidget *focus_child = gtk_widget_get_focus_child (widget);
421   int i;
422   gboolean ret = FALSE;
423 
424   focus_order = g_ptr_array_new ();
425   focus_sort (widget, direction, focus_order);
426 
427   for (i = 0; i < focus_order->len && !ret; i++) {
428     GtkWidget *child = g_ptr_array_index (focus_order, i);
429 
430     if (focus_child) {
431       if (focus_child == child) {
432         focus_child = NULL;
433         ret = gtk_widget_child_focus (child, direction);
434       }
435     } else if (gtk_widget_get_mapped (child) &&
436                gtk_widget_is_ancestor (child, widget)) {
437       ret = gtk_widget_child_focus (child, direction);
438     }
439   }
440 
441   g_ptr_array_unref (focus_order);
442 
443   return ret;
444 }
445 
446 gboolean
adw_widget_focus_child(GtkWidget * widget,GtkDirectionType direction)447 adw_widget_focus_child (GtkWidget        *widget,
448                         GtkDirectionType  direction)
449 {
450   return focus_move (widget, direction);
451 }
452 
453 gboolean
adw_widget_grab_focus_self(GtkWidget * widget)454 adw_widget_grab_focus_self (GtkWidget *widget)
455 {
456   if (!gtk_widget_get_focusable (widget))
457     return FALSE;
458 
459   gtk_root_set_focus (gtk_widget_get_root (widget), widget);
460 
461   return TRUE;
462 }
463 
464 gboolean
adw_widget_grab_focus_child(GtkWidget * widget)465 adw_widget_grab_focus_child (GtkWidget *widget)
466 {
467   GtkWidget *child;
468 
469   for (child = gtk_widget_get_first_child (widget);
470        child != NULL;
471        child = gtk_widget_get_next_sibling (child))
472     if (gtk_widget_grab_focus (child))
473       return TRUE;
474 
475   return FALSE;
476 }
477 
478 void
adw_widget_compute_expand(GtkWidget * widget,gboolean * hexpand_p,gboolean * vexpand_p)479 adw_widget_compute_expand (GtkWidget *widget,
480                            gboolean  *hexpand_p,
481                            gboolean  *vexpand_p)
482 {
483   GtkWidget *child;
484   gboolean hexpand = FALSE;
485   gboolean vexpand = FALSE;
486 
487   for (child = gtk_widget_get_first_child (widget);
488        child != NULL;
489        child = gtk_widget_get_next_sibling (child)) {
490     hexpand = hexpand || gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL);
491     vexpand = vexpand || gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL);
492   }
493 
494   *hexpand_p = hexpand;
495   *vexpand_p = vexpand;
496 }
497 
498 void
adw_widget_compute_expand_horizontal_only(GtkWidget * widget,gboolean * hexpand_p,gboolean * vexpand_p)499 adw_widget_compute_expand_horizontal_only (GtkWidget *widget,
500                                            gboolean  *hexpand_p,
501                                            gboolean  *vexpand_p)
502 {
503   GtkWidget *child;
504   gboolean hexpand = FALSE;
505 
506   for (child = gtk_widget_get_first_child (widget);
507        child != NULL;
508        child = gtk_widget_get_next_sibling (child))
509     hexpand = hexpand || gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL);
510 
511   *hexpand_p = hexpand;
512   *vexpand_p = FALSE;
513 }
514 
515 GtkSizeRequestMode
adw_widget_get_request_mode(GtkWidget * widget)516 adw_widget_get_request_mode (GtkWidget *widget)
517 {
518   GtkWidget *child;
519   int wfh = 0, hfw = 0;
520 
521   for (child = gtk_widget_get_first_child (widget);
522        child;
523        child = gtk_widget_get_next_sibling (child)) {
524     GtkSizeRequestMode mode = gtk_widget_get_request_mode (child);
525 
526     switch (mode) {
527     case GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH:
528       hfw++;
529       break;
530     case GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT:
531       wfh++;
532       break;
533     case GTK_SIZE_REQUEST_CONSTANT_SIZE:
534     default:
535       break;
536     }
537   }
538 
539   if (hfw == 0 && wfh == 0)
540     return GTK_SIZE_REQUEST_CONSTANT_SIZE;
541   else
542     return wfh > hfw ?
543         GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT :
544         GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
545 }
546