gnome-shell/src/st/st-container.c
Dan Winship d2b968a7df St: add keyboard focus navigation support
Add StWidget:can-focus, st_widget_navigate_focus(), and
st_container_get_focus_chain(), and implement as needed to allow
keyboard navigation of widgets.

https://bugzilla.gnome.org/show_bug.cgi?id=621671
2010-10-29 08:38:05 -04:00

743 lines
21 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* st-container.c: Base class for St container actors
*
* Copyright 2007 OpenedHand
* Copyright 2008, 2009 Intel Corporation.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU Lesser General Public License,
* version 2.1, as published by the Free Software Foundation.
*
* This program is distributed in the hope 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 program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
* Boston, MA 02111-1307, USA.
*
* Written by: Emmanuele Bassi <ebassi@openedhand.com>
* Thomas Wood <thomas@linux.intel.com>
*
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include "st-container.h"
#define ST_CONTAINER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj),ST_TYPE_CONTAINER, StContainerPrivate))
struct _StContainerPrivate
{
GList *children;
ClutterActor *first_child;
ClutterActor *last_child;
};
static void clutter_container_iface_init (ClutterContainerIface *iface);
G_DEFINE_ABSTRACT_TYPE_WITH_CODE (StContainer, st_container, ST_TYPE_WIDGET,
G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
clutter_container_iface_init));
static void
st_container_update_pseudo_classes (StContainer *container)
{
GList *first_item, *last_item;
ClutterActor *first_child, *last_child;
StContainerPrivate *priv = container->priv;
first_item = priv->children;
first_child = first_item ? first_item->data : NULL;
if (first_child != priv->first_child)
{
if (priv->first_child && ST_IS_WIDGET (priv->first_child))
st_widget_remove_style_pseudo_class (ST_WIDGET (priv->first_child),
"first-child");
if (priv->first_child)
{
g_object_unref (priv->first_child);
priv->first_child = NULL;
}
if (first_child && ST_IS_WIDGET (first_child))
st_widget_add_style_pseudo_class (ST_WIDGET (first_child),
"first-child");
if (first_child)
priv->first_child = g_object_ref (first_child);
}
last_item = g_list_last (priv->children);
last_child = last_item ? last_item->data : NULL;
if (last_child != priv->last_child)
{
if (priv->last_child && ST_IS_WIDGET (priv->last_child))
st_widget_remove_style_pseudo_class (ST_WIDGET (priv->last_child),
"last-child");
if (priv->last_child)
{
g_object_unref (priv->last_child);
priv->last_child = NULL;
}
if (last_child && ST_IS_WIDGET (last_child))
st_widget_add_style_pseudo_class (ST_WIDGET (last_child),
"last-child");
if (last_child)
priv->last_child = g_object_ref (last_child);
}
}
/**
* st_container_remove_all:
* @container: An #StContainer
*
* Removes all child actors from @container.
*/
void
st_container_remove_all (StContainer *container)
{
StContainerPrivate *priv = container->priv;
/* copied from clutter_group_remove_all() */
while (priv->children)
{
ClutterActor *child = priv->children->data;
priv->children = priv->children->next;
clutter_container_remove_actor (CLUTTER_CONTAINER (container), child);
}
st_container_update_pseudo_classes (container);
}
/**
* st_container_destroy_children:
* @container: An #StContainer
*
* Destroys all child actors from @container.
*/
void
st_container_destroy_children (StContainer *container)
{
StContainerPrivate *priv = container->priv;
while (priv->children)
{
ClutterActor *child = priv->children->data;
priv->children = priv->children->next;
clutter_actor_destroy (child);
}
}
void
st_container_move_child (StContainer *container,
ClutterActor *actor,
int pos)
{
StContainerPrivate *priv = container->priv;
GList *item = NULL;
item = g_list_find (priv->children, actor);
if (item == NULL)
{
g_warning ("Actor of type '%s' is not a child of the %s container",
g_type_name (G_OBJECT_TYPE (actor)),
g_type_name (G_OBJECT_TYPE (container)));
return;
}
priv->children = g_list_delete_link (priv->children, item);
priv->children = g_list_insert (priv->children, actor, pos);
st_container_update_pseudo_classes (container);
clutter_actor_queue_relayout ((ClutterActor*) container);
}
/**
* st_container_get_children_list:
* @container: An #StContainer
*
* Get the internal list of @container's child actors. This function
* should only be used by subclasses of StContainer
*
* Returns: (element-type Clutter.Actor) (transfer none): list of @container's child actors
*/
GList *
st_container_get_children_list (StContainer *container)
{
g_return_val_if_fail (ST_IS_CONTAINER (container), NULL);
return container->priv->children;
}
static GList *
st_container_real_get_focus_chain (StContainer *container)
{
GList *chain, *children;
chain = NULL;
for (children = container->priv->children; children; children = children->next)
{
ClutterActor *child = children->data;
if (CLUTTER_ACTOR_IS_VISIBLE (child))
chain = g_list_prepend (chain, child);
}
return g_list_reverse (chain);
}
/**
* st_container_get_focus_chain:
* @container: An #StContainer
*
* Gets a list of the focusable children of @container, in "Tab"
* order. By default, this returns all visible
* (as in CLUTTER_ACTOR_IS_VISIBLE()) children of @container.
*
* Returns: (element-type Clutter.Actor) (transfer container):
* @container's focusable children
*/
GList *
st_container_get_focus_chain (StContainer *container)
{
return ST_CONTAINER_GET_CLASS (container)->get_focus_chain (container);
}
static gint
sort_z_order (gconstpointer a,
gconstpointer b)
{
float depth_a, depth_b;
depth_a = clutter_actor_get_depth (CLUTTER_ACTOR (a));
depth_b = clutter_actor_get_depth (CLUTTER_ACTOR (b));
if (depth_a < depth_b)
return -1;
if (depth_a > depth_b)
return 1;
return 0;
}
static void
st_container_add (ClutterContainer *container,
ClutterActor *actor)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
g_object_ref (actor);
priv->children = g_list_append (priv->children, actor);
clutter_actor_set_parent (actor, CLUTTER_ACTOR (container));
/* queue a relayout, to get the correct positioning inside
* the ::actor-added signal handlers
*/
clutter_actor_queue_relayout (CLUTTER_ACTOR (container));
g_signal_emit_by_name (container, "actor-added", actor);
clutter_container_sort_depth_order (container);
st_container_update_pseudo_classes (ST_CONTAINER (container));
g_object_unref (actor);
}
static void
st_container_remove (ClutterContainer *container,
ClutterActor *actor)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
g_object_ref (actor);
priv->children = g_list_remove (priv->children, actor);
clutter_actor_unparent (actor);
/* queue a relayout, to get the correct positioning inside
* the ::actor-removed signal handlers
*/
clutter_actor_queue_relayout (CLUTTER_ACTOR (container));
/* at this point, the actor passed to the "actor-removed" signal
* handlers is not parented anymore to the container but since we
* are holding a reference on it, it's still valid
*/
g_signal_emit_by_name (container, "actor-removed", actor);
st_container_update_pseudo_classes (ST_CONTAINER (container));
if (CLUTTER_ACTOR_IS_VISIBLE (container))
clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
g_object_unref (actor);
}
static void
st_container_foreach (ClutterContainer *container,
ClutterCallback callback,
gpointer user_data)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
/* Using g_list_foreach instead of iterating the list manually
* because it has better protection against the current node being
* removed. This will happen for example if someone calls
* clutter_container_foreach(container, clutter_actor_destroy)
*/
g_list_foreach (priv->children, (GFunc) callback, user_data);
}
static void
st_container_raise (ClutterContainer *container,
ClutterActor *actor,
ClutterActor *sibling)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
priv->children = g_list_remove (priv->children, actor);
/* Raise at the top */
if (!sibling)
{
GList *last_item;
last_item = g_list_last (priv->children);
if (last_item)
sibling = last_item->data;
priv->children = g_list_append (priv->children, actor);
}
else
{
gint pos;
pos = g_list_index (priv->children, sibling) + 1;
priv->children = g_list_insert (priv->children, actor, pos);
}
/* set Z ordering a value below, this will then call sort
* as values are equal ordering shouldn't change but Z
* values will be correct.
*
* FIXME: optimise
*/
if (sibling &&
clutter_actor_get_depth (sibling) != clutter_actor_get_depth (actor))
{
clutter_actor_set_depth (actor, clutter_actor_get_depth (sibling));
}
st_container_update_pseudo_classes (ST_CONTAINER (container));
if (CLUTTER_ACTOR_IS_VISIBLE (container))
clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}
static void
st_container_lower (ClutterContainer *container,
ClutterActor *actor,
ClutterActor *sibling)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
priv->children = g_list_remove (priv->children, actor);
/* Push to bottom */
if (!sibling)
{
GList *last_item;
last_item = g_list_first (priv->children);
if (last_item)
sibling = last_item->data;
priv->children = g_list_prepend (priv->children, actor);
}
else
{
gint pos;
pos = g_list_index (priv->children, sibling);
priv->children = g_list_insert (priv->children, actor, pos);
}
/* See comment in st_container_raise() for this */
if (sibling &&
clutter_actor_get_depth (sibling) != clutter_actor_get_depth (actor))
{
clutter_actor_set_depth (actor, clutter_actor_get_depth (sibling));
}
st_container_update_pseudo_classes (ST_CONTAINER (container));
if (CLUTTER_ACTOR_IS_VISIBLE (container))
clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}
static void
st_container_sort_depth_order (ClutterContainer *container)
{
StContainerPrivate *priv = ST_CONTAINER (container)->priv;
priv->children = g_list_sort (priv->children, sort_z_order);
if (CLUTTER_ACTOR_IS_VISIBLE (container))
clutter_actor_queue_redraw (CLUTTER_ACTOR (container));
}
static void
st_container_dispose (GObject *object)
{
StContainerPrivate *priv = ST_CONTAINER (object)->priv;
if (priv->children)
{
g_list_foreach (priv->children, (GFunc) clutter_actor_destroy, NULL);
g_list_free (priv->children);
priv->children = NULL;
}
if (priv->first_child)
g_object_unref (priv->first_child);
priv->first_child = NULL;
if (priv->last_child)
g_object_unref (priv->last_child);
priv->last_child = NULL;
G_OBJECT_CLASS (st_container_parent_class)->dispose (object);
}
/* filter @children to contain only only actors that overlap @rbox
* when moving in @direction. (Assuming no transformations.)
*/
static GList *
filter_by_position (GList *children,
ClutterActorBox *rbox,
GtkDirectionType direction)
{
ClutterActorBox cbox;
GList *l, *ret;
ClutterActor *child;
for (l = children, ret = NULL; l; l = l->next)
{
child = l->data;
clutter_actor_get_allocation_box (child, &cbox);
/* Filter out children if they are in the wrong direction from
* @rbox, or if they don't overlap it.
*/
switch (direction)
{
case GTK_DIR_UP:
if (cbox.y2 > rbox->y1)
continue;
if (cbox.x1 >= rbox->x2 || cbox.x2 <= rbox->x1)
continue;
break;
case GTK_DIR_DOWN:
if (cbox.y1 < rbox->y2)
continue;
if (cbox.x1 >= rbox->x2 || cbox.x2 <= rbox->x1)
continue;
break;
case GTK_DIR_LEFT:
if (cbox.x2 > rbox->x1)
continue;
if (cbox.y1 >= rbox->y2 || cbox.y2 <= rbox->y1)
continue;
break;
case GTK_DIR_RIGHT:
if (cbox.x1 < rbox->x2)
continue;
if (cbox.y1 >= rbox->y2 || cbox.y2 <= rbox->y1)
continue;
break;
default:
g_return_val_if_reached (NULL);
}
ret = g_list_prepend (ret, child);
}
g_list_free (children);
return ret;
}
typedef struct {
GtkDirectionType direction;
ClutterActorBox box;
} StContainerChildSortData;
static int
sort_by_position (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
ClutterActor *actor_a = (ClutterActor *)a;
ClutterActor *actor_b = (ClutterActor *)b;
StContainerChildSortData *sort_data = user_data;
GtkDirectionType direction = sort_data->direction;
ClutterActorBox abox, bbox;
int ax, ay, bx, by;
int cmp, fmid;
/* Determine the relationship, relative to motion in @direction, of
* the center points of the two actors. Eg, for %GTK_DIR_UP, we
* return a negative number if @actor_a's center is below @actor_b's
* center, and postive if vice versa, which will result in an
* overall list sorted bottom-to-top.
*/
clutter_actor_get_allocation_box (actor_a, &abox);
ax = (int)(abox.x1 + abox.x2) / 2;
ay = (int)(abox.y1 + abox.y2) / 2;
clutter_actor_get_allocation_box (actor_b, &bbox);
bx = (int)(bbox.x1 + bbox.x2) / 2;
by = (int)(bbox.y1 + bbox.y2) / 2;
switch (direction)
{
case GTK_DIR_UP:
cmp = by - ay;
break;
case GTK_DIR_DOWN:
cmp = ay - by;
break;
case GTK_DIR_LEFT:
cmp = bx - ax;
break;
case GTK_DIR_RIGHT:
cmp = ax - bx;
break;
default:
g_return_val_if_reached (0);
}
if (cmp)
return cmp;
/* If two actors have the same center on the axis being sorted,
* prefer the one that is closer to the center of the current focus
* actor on the other axis. Eg, for %GTK_DIR_UP, prefer whichever
* of @actor_a and @actor_b has a horizontal center closest to the
* current focus actor's horizontal center.
*
* (This matches GTK's behavior.)
*/
switch (direction)
{
case GTK_DIR_UP:
case GTK_DIR_DOWN:
fmid = (int)(sort_data->box.x1 + sort_data->box.x2) / 2;
return abs (ax - fmid) - abs (bx - fmid);
case GTK_DIR_LEFT:
case GTK_DIR_RIGHT:
fmid = (int)(sort_data->box.y1 + sort_data->box.y2) / 2;
return abs (ay - fmid) - abs (by - fmid);
default:
g_return_val_if_reached (0);
}
}
static gboolean
st_container_navigate_focus (StWidget *widget,
ClutterActor *from,
GtkDirectionType direction)
{
StContainer *container = ST_CONTAINER (widget);
ClutterActor *container_actor, *focus_child;
GList *children, *l;
container_actor = CLUTTER_ACTOR (widget);
if (from == container_actor)
return FALSE;
/* Figure out if @from is a descendant of @container, and if so,
* set @focus_child to the immediate child of @container that
* contains (or *is*) @from.
*/
focus_child = from;
while (focus_child && clutter_actor_get_parent (focus_child) != container_actor)
focus_child = clutter_actor_get_parent (focus_child);
if (st_widget_get_can_focus (widget))
{
if (!focus_child)
{
/* Accept focus from outside */
clutter_actor_grab_key_focus (container_actor);
return TRUE;
}
else
{
/* Yield focus from within: since @container itself is
* focusable we don't allow the focus to be navigated
* within @container.
*/
return FALSE;
}
}
/* See if we can navigate within @focus_child */
if (focus_child && ST_IS_WIDGET (focus_child))
{
if (st_widget_navigate_focus (ST_WIDGET (focus_child), from, direction, FALSE))
return TRUE;
}
/* At this point we know that we want to navigate focus to one of
* @container's immediate children; the next one after @focus_child,
* or the first one if @focus_child is %NULL. (With "next" and
* "first" being determined by @direction.)
*/
children = st_container_get_focus_chain (container);
if (direction == GTK_DIR_TAB_FORWARD ||
direction == GTK_DIR_TAB_BACKWARD)
{
if (direction == GTK_DIR_TAB_BACKWARD)
children = g_list_reverse (children);
if (focus_child)
{
/* Remove focus_child and any earlier children */
while (children && children->data != focus_child)
children = g_list_delete_link (children, children);
if (children)
children = g_list_delete_link (children, children);
}
}
else /* direction is an arrow key, not tab */
{
StContainerChildSortData sort_data;
/* Compute the allocation box of the previous focused actor, in
* @container's coordinate space. If there was no previous focus,
* use the coordinates of the appropriate edge of @container.
*
* Note that all of this code assumes the actors are not
* transformed (or at most, they are all scaled by the same
* amount). If @container or any of its children is rotated, or
* any child is inconsistently scaled, then the focus chain will
* probably be unpredictable.
*/
if (from)
{
if (from == focus_child)
clutter_actor_get_allocation_box (focus_child, &sort_data.box);
else
{
float cx, cy, fx, fy, fw, fh;
clutter_actor_get_transformed_position (CLUTTER_ACTOR (container), &cx, &cy);
clutter_actor_get_transformed_position (from, &fx, &fy);
clutter_actor_get_transformed_size (from, &fw, &fh);
sort_data.box.x1 = fx - cx;
sort_data.box.x2 = fx - cx + fw;
sort_data.box.y1 = fy - cy;
sort_data.box.y2 = fy - cy + fh;
}
}
else
{
clutter_actor_get_allocation_box (CLUTTER_ACTOR (container), &sort_data.box);
switch (direction)
{
case GTK_DIR_UP:
sort_data.box.y1 = sort_data.box.y2;
break;
case GTK_DIR_DOWN:
sort_data.box.y2 = sort_data.box.y1;
break;
case GTK_DIR_LEFT:
sort_data.box.x1 = sort_data.box.x2;
break;
case GTK_DIR_RIGHT:
sort_data.box.x2 = sort_data.box.x1;
break;
default:
g_warn_if_reached ();
}
}
sort_data.direction = direction;
if (focus_child)
children = filter_by_position (children, &sort_data.box, direction);
if (children)
children = g_list_sort_with_data (children, sort_by_position, &sort_data);
}
/* Now try each child in turn */
for (l = children; l; l = l->next)
{
if (ST_IS_WIDGET (l->data))
{
if (st_widget_navigate_focus (l->data, from, direction, FALSE))
{
g_list_free (children);
return TRUE;
}
}
}
g_list_free (children);
return FALSE;
}
static void
clutter_container_iface_init (ClutterContainerIface *iface)
{
iface->add = st_container_add;
iface->remove = st_container_remove;
iface->foreach = st_container_foreach;
iface->raise = st_container_raise;
iface->lower = st_container_lower;
iface->sort_depth_order = st_container_sort_depth_order;
}
static void
st_container_init (StContainer *container)
{
container->priv = ST_CONTAINER_GET_PRIVATE (container);
}
static void
st_container_class_init (StContainerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
StWidgetClass *widget_class = ST_WIDGET_CLASS (klass);
StContainerClass *container_class = ST_CONTAINER_CLASS (klass);
g_type_class_add_private (klass, sizeof (StContainerPrivate));
object_class->dispose = st_container_dispose;
widget_class->navigate_focus = st_container_navigate_focus;
container_class->get_focus_chain = st_container_real_get_focus_chain;
}