Implement tab/up-down/left-right focus sorting for widgets

basically do what GtkContainer already did.
This commit is contained in:
Timm Bäder 2017-02-10 21:21:38 +01:00
parent 6e9d857714
commit c9fc201f77
4 changed files with 510 additions and 24 deletions

View File

@ -931,6 +931,7 @@ gtk_base_c_sources = \
gtkvolumebutton.c \
gtkviewport.c \
gtkwidget.c \
gtkwidgetfocus.c \
gtkwidgetpath.c \
gtkwindow.c \
gtkwindowgroup.c \

View File

@ -7318,36 +7318,25 @@ gtk_widget_real_focus (GtkWidget *widget,
return TRUE;
}
}
else if (_gtk_widget_get_first_child (widget) == NULL)
{
/* No children, no possibility to focus anything */
return FALSE;
}
else
{
/* @widget can't be focused, but maybe one of its child widgets. */
GtkWidget *focus_child = gtk_widget_get_focus_child (widget);
GtkWidget *child;
GPtrArray *focus_order = g_ptr_array_new ();
gboolean ret = FALSE;
if (focus_child != NULL)
{
if (gtk_widget_child_focus (focus_child, direction))
return TRUE;
/* Try focusing any of the child widgets, depending on the given @direction */
child = focus_child;
}
else
{
child = _gtk_widget_get_first_child (widget);
}
gtk_widget_focus_sort (widget, direction, focus_order);
ret = gtk_widget_focus_move (widget, direction, focus_order);
/* The current focus child didn't handle the focus, so lets'
try all its siblings. If none of them accepts it, we simply
have to return FALSE since we couldn't handle it either. */
for (;
child != NULL;
child = _gtk_widget_get_next_sibling (child))
{
g_ptr_array_unref (focus_order);
if (_gtk_widget_is_drawable (child) &&
gtk_widget_child_focus (child, direction))
return TRUE;
}
if (ret)
return TRUE;
}
return FALSE;

489
gtk/gtkwidgetfocus.c Normal file
View File

@ -0,0 +1,489 @@
/* GTK - The GIMP Toolkit
* Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
#include "gtkwidgetprivate.h"
#include "gtkwindow.h"
typedef struct _CompareInfo CompareInfo;
struct _CompareInfo
{
GtkWidget *widget;
int x;
int y;
gboolean reverse : 1;
};
/* Utility function, equivalent to g_list_reverse */
static void
reverse_ptr_array (GPtrArray *arr)
{
int i;
for (i = 0; i < arr->len / 2; i ++)
{
void *a = g_ptr_array_index (arr, i);
void *b = g_ptr_array_index (arr, arr->len - 1 - i);
arr->pdata[i] = b;
arr->pdata[arr->len - 1 - i] = a;
}
}
/* Get coordinates of @widget's allocation with respect to
* allocation of @container.
*/
static gboolean
get_allocation_coords (GtkWidget *widget,
GtkWidget *child,
GdkRectangle *allocation)
{
gtk_widget_get_allocation (child, allocation);
return gtk_widget_translate_coordinates (child, widget,
0, 0, &allocation->x, &allocation->y);
}
static int
tab_sort_func (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
GtkAllocation child1_allocation, child2_allocation;
const GtkWidget *child1 = *((GtkWidget **)a);
const GtkWidget *child2 = *((GtkWidget **)b);
GtkTextDirection text_direction = GPOINTER_TO_INT (user_data);
int y1, y2;
_gtk_widget_get_allocation ((GtkWidget *) child1, &child1_allocation);
_gtk_widget_get_allocation ((GtkWidget *) child2, &child2_allocation);
y1 = child1_allocation.y + child1_allocation.height / 2;
y2 = child2_allocation.y + child2_allocation.height / 2;
if (y1 == y2)
{
int x1 = child1_allocation.x + child1_allocation.width / 2;
int x2 = child2_allocation.x + child2_allocation.width / 2;
if (text_direction == GTK_TEXT_DIR_RTL)
return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1);
else
return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1);
}
else
return (y1 < y2) ? -1 : 1;
}
static void
focus_sort_tab (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order)
{
GtkTextDirection text_direction = _gtk_widget_get_direction (widget);
g_ptr_array_sort_with_data (focus_order, tab_sort_func, GINT_TO_POINTER (text_direction));
if (direction == GTK_DIR_TAB_BACKWARD)
reverse_ptr_array (focus_order);
}
/* Look for a child in @children that is intermediate between
* the focus widget and container. This widget, if it exists,
* acts as the starting widget for focus navigation.
*/
static GtkWidget *
find_old_focus (GtkWidget *widget,
GPtrArray *children)
{
int i;
for (i = 0; i < children->len; i ++)
{
GtkWidget *child = g_ptr_array_index (children, i);
GtkWidget *child_ptr = child;
while (child_ptr && child_ptr != widget)
{
GtkWidget *parent;
parent = _gtk_widget_get_parent (child_ptr);
if (parent && (gtk_widget_get_focus_child (parent) != child_ptr))
{
child = NULL;
break;
}
child_ptr = parent;
}
if (child)
return child;
}
return NULL;
}
static gboolean
old_focus_coords (GtkWidget *widget,
GdkRectangle *old_focus_rect)
{
GtkWidget *toplevel = _gtk_widget_get_toplevel (widget);
GtkWidget *old_focus;
if (GTK_IS_WINDOW (toplevel))
{
old_focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
if (old_focus)
return get_allocation_coords (widget, old_focus, old_focus_rect);
}
return FALSE;
}
static int
left_right_compare (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
const GtkWidget *child1 = *((GtkWidget **)a);
const GtkWidget *child2 = *((GtkWidget **)b);
GdkRectangle allocation1;
GdkRectangle allocation2;
CompareInfo *compare = user_data;
int x1, x2;
get_allocation_coords (compare->widget, (GtkWidget *)child1, &allocation1);
get_allocation_coords (compare->widget, (GtkWidget *)child2, &allocation2);
x1 = allocation1.x + allocation1.width / 2;
x2 = allocation2.x + allocation2.width / 2;
if (x1 == x2)
{
int y1 = abs (allocation1.y + allocation1.height / 2 - compare->y);
int y2 = abs (allocation2.y + allocation2.height / 2 - compare->y);
if (compare->reverse)
return (y1 < y2) ? 1 : ((y1 == y2) ? 0 : -1);
else
return (y1 < y2) ? -1 : ((y1 == y2) ? 0 : 1);
}
else
return (x1 < x2) ? -1 : 1;
}
static int
up_down_compare (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
const GtkWidget *child1 = *((GtkWidget **)a);
const GtkWidget *child2 = *((GtkWidget **)b);
GdkRectangle allocation1;
GdkRectangle allocation2;
CompareInfo *compare = user_data;
int y1, y2;
get_allocation_coords (compare->widget, (GtkWidget *)child1, &allocation1);
get_allocation_coords (compare->widget, (GtkWidget *)child2, &allocation2);
y1 = allocation1.y + allocation1.height / 2;
y2 = allocation2.y + allocation2.height / 2;
if (y1 == y2)
{
int x1 = abs (allocation1.x + allocation1.width / 2 - compare->x);
int x2 = abs (allocation2.x + allocation2.width / 2 - compare->x);
if (compare->reverse)
return (x1 < x2) ? 1 : ((x1 == x2) ? 0 : -1);
else
return (x1 < x2) ? -1 : ((x1 == x2) ? 0 : 1);
}
else
return (y1 < y2) ? -1 : 1;
}
static void
focus_sort_left_right (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order)
{
CompareInfo compare_info;
GdkRectangle old_allocation;
GtkWidget *old_focus = gtk_widget_get_focus_child (widget);
compare_info.widget = widget;
compare_info.reverse = (direction == GTK_DIR_LEFT);
if (!old_focus)
old_focus = find_old_focus (widget, focus_order);
if (old_focus && get_allocation_coords (widget, old_focus, &old_allocation))
{
int compare_y1;
int compare_y2;
int compare_x;
int i;
/* Delete widgets from list that don't match minimum criteria */
compare_y1 = old_allocation.y;
compare_y2 = old_allocation.y + old_allocation.height;
if (direction == GTK_DIR_LEFT)
compare_x = old_allocation.x;
else
compare_x = old_allocation.x + old_allocation.width;
for (i = 0; i < focus_order->len; i ++)
{
GtkWidget *child = g_ptr_array_index (focus_order, i);
int child_y1, child_y2;
GdkRectangle child_allocation;
if (child != old_focus)
{
if (get_allocation_coords (widget, child, &child_allocation))
{
child_y1 = child_allocation.y;
child_y2 = child_allocation.y + child_allocation.height;
if ((child_y2 <= compare_y1 || child_y1 >= compare_y2) /* No vertical overlap */ ||
(direction == GTK_DIR_RIGHT && child_allocation.x + child_allocation.width < compare_x) || /* Not to left */
(direction == GTK_DIR_LEFT && child_allocation.x > compare_x)) /* Not to right */
{
g_ptr_array_remove_index (focus_order, i);
i --;
}
}
else
{
g_ptr_array_remove_index (focus_order, i);
i --;
}
}
}
compare_info.y = (compare_y1 + compare_y2) / 2;
compare_info.x = old_allocation.x + old_allocation.width / 2;
}
else
{
/* No old focus widget, need to figure out starting x,y some other way
*/
GtkAllocation allocation;
GdkRectangle old_focus_rect;
_gtk_widget_get_allocation (widget, &allocation);
if (old_focus_coords (widget, &old_focus_rect))
{
compare_info.y = old_focus_rect.y + old_focus_rect.height / 2;
}
else
{
if (!_gtk_widget_get_has_window (widget))
compare_info.y = allocation.y + allocation.height / 2;
else
compare_info.y = allocation.height / 2;
}
if (!_gtk_widget_get_has_window (widget))
compare_info.x = (direction == GTK_DIR_RIGHT) ? allocation.x : allocation.x + allocation.width;
else
compare_info.x = (direction == GTK_DIR_RIGHT) ? 0 : allocation.width;
}
g_ptr_array_sort_with_data (focus_order, left_right_compare, &compare_info);
if (compare_info.reverse)
reverse_ptr_array (focus_order);
}
static void
focus_sort_up_down (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order)
{
CompareInfo compare_info;
GdkRectangle old_allocation;
GtkWidget *old_focus = gtk_widget_get_focus_child (widget);
compare_info.widget = widget;
compare_info.reverse = (direction == GTK_DIR_UP);
if (!old_focus)
old_focus = find_old_focus (widget, focus_order);
if (old_focus && get_allocation_coords (widget, old_focus, &old_allocation))
{
int compare_x1;
int compare_x2;
int compare_y;
int i;
/* Delete widgets from list that don't match minimum criteria */
compare_x1 = old_allocation.x;
compare_x2 = old_allocation.x + old_allocation.width;
if (direction == GTK_DIR_UP)
compare_y = old_allocation.y;
else
compare_y = old_allocation.y + old_allocation.height;
for (i = 0; i < focus_order->len; i ++)
{
GtkWidget *child = g_ptr_array_index (focus_order, i);
int child_x1, child_x2;
GdkRectangle child_allocation;
if (child != old_focus)
{
if (get_allocation_coords (widget, child, &child_allocation))
{
child_x1 = child_allocation.x;
child_x2 = child_allocation.x + child_allocation.width;
if ((child_x2 <= compare_x1 || child_x1 >= compare_x2) /* No horizontal overlap */ ||
(direction == GTK_DIR_DOWN && child_allocation.y + child_allocation.height < compare_y) || /* Not below */
(direction == GTK_DIR_UP && child_allocation.y > compare_y)) /* Not above */
{
g_ptr_array_remove_index (focus_order, i);
i --;
}
}
else
{
g_ptr_array_remove_index (focus_order, i);
i --;
}
}
}
compare_info.x = (compare_x1 + compare_x2) / 2;
compare_info.y = old_allocation.y + old_allocation.height / 2;
}
else
{
/* No old focus widget, need to figure out starting x,y some other way
*/
GtkAllocation allocation;
GdkRectangle old_focus_rect;
_gtk_widget_get_allocation (widget, &allocation);
if (old_focus_coords (widget, &old_focus_rect))
{
compare_info.x = old_focus_rect.x + old_focus_rect.width / 2;
}
else
{
if (!_gtk_widget_get_has_window (widget))
compare_info.x = allocation.x + allocation.width / 2;
else
compare_info.x = allocation.width / 2;
}
if (!_gtk_widget_get_has_window (widget))
compare_info.y = (direction == GTK_DIR_DOWN) ? allocation.y : allocation.y + allocation.height;
else
compare_info.y = (direction == GTK_DIR_DOWN) ? 0 : + allocation.height;
}
g_ptr_array_sort_with_data (focus_order, up_down_compare, &compare_info);
if (compare_info.reverse)
reverse_ptr_array (focus_order);
}
void
gtk_widget_focus_sort (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order)
{
GtkWidget *child;
g_assert (focus_order->len == 0);
/* Initialize the list with all realized child widgets */
for (child = _gtk_widget_get_first_child (widget);
child != NULL;
child = _gtk_widget_get_next_sibling (child))
{
if (_gtk_widget_get_realized (child))
g_ptr_array_add (focus_order, child);
}
/* Now sort that list depending on @direction */
switch (direction)
{
case GTK_DIR_TAB_FORWARD:
case GTK_DIR_TAB_BACKWARD:
focus_sort_tab (widget, direction, focus_order);
break;
case GTK_DIR_UP:
case GTK_DIR_DOWN:
focus_sort_up_down (widget, direction, focus_order);
break;
case GTK_DIR_LEFT:
case GTK_DIR_RIGHT:
focus_sort_left_right (widget, direction, focus_order);
break;
default:
g_assert_not_reached ();
}
}
gboolean
gtk_widget_focus_move (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order)
{
GtkWidget *focus_child = gtk_widget_get_focus_child (widget);
int i;
for (i = 0; i < focus_order->len; i ++)
{
GtkWidget *child = g_ptr_array_index (focus_order, i);
if (focus_child)
{
if (focus_child == child)
{
focus_child = NULL;
if (gtk_widget_child_focus (child, direction))
return TRUE;
}
}
else if (_gtk_widget_is_drawable (child) &&
gtk_widget_is_ancestor (child, widget))
{
if (gtk_widget_child_focus (child, direction))
return TRUE;
}
}
return FALSE;
}

View File

@ -307,6 +307,13 @@ void gtk_widget_forall (GtkWidget
GtkWidget *gtk_widget_get_focus_child (GtkWidget *widget);
void gtk_widget_focus_sort (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order);
gboolean gtk_widget_focus_move (GtkWidget *widget,
GtkDirectionType direction,
GPtrArray *focus_order);
/* inline getters */
static inline gboolean