gnome-shell/src/st/st-widget.c
Rui Matos 02aae631d8 st-widget: Allow diagonal moves for directional keynav
This allows us to do directional keyboard navigation when there's no
actor inside the horizontal or vertical strip extending from the
origin actor but there are other actors to the sides of that strip
that could still be used as targets even if that means the focus would
move diagonally.

https://bugzilla.gnome.org/show_bug.cgi?id=663901
2012-03-10 15:00:18 +01:00

2646 lines
76 KiB
C

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* st-widget.c: Base class for St actors
*
* Copyright 2007 OpenedHand
* Copyright 2008, 2009 Intel Corporation.
* Copyright 2009, 2010 Red Hat, Inc.
* Copyright 2009 Abderrahim Kitouni
* Copyright 2009, 2010 Florian Müllner
* Copyright 2010 Adel Gadllah
* Copyright 2012 Igalia, S.L.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <clutter/clutter.h>
#include "st-widget.h"
#include "st-label.h"
#include "st-private.h"
#include "st-texture-cache.h"
#include "st-theme-context.h"
#include "st-theme-node-transition.h"
#include "st-widget-accessible.h"
#include <gtk/gtk.h>
#include <atk/atk-enum-types.h>
/*
* Forward declaration for sake of StWidgetChild
*/
struct _StWidgetPrivate
{
StTheme *theme;
StThemeNode *theme_node;
gchar *pseudo_class;
gchar *style_class;
gchar *inline_style;
StThemeNodeTransition *transition_animation;
gboolean is_stylable : 1;
gboolean is_style_dirty : 1;
gboolean draw_bg_color : 1;
gboolean draw_border_internal : 1;
gboolean track_hover : 1;
gboolean hover : 1;
gboolean can_focus : 1;
AtkObject *accessible;
AtkRole accessible_role;
AtkStateSet *local_state_set;
ClutterActor *label_actor;
/* Even though Clutter has first_child/last_child properties,
* we need to keep track of the old first/last children so
* that we can remove the pseudo classes on them. */
StWidget *prev_last_child;
StWidget *prev_first_child;
};
/**
* SECTION:st-widget
* @short_description: Base class for stylable actors
*
* #StWidget is a simple abstract class on top of #ClutterActor. It
* provides basic themeing properties.
*
* Actors in the St library should subclass #StWidget if they plan
* to obey to a certain #StStyle.
*/
enum
{
PROP_0,
PROP_THEME,
PROP_PSEUDO_CLASS,
PROP_STYLE_CLASS,
PROP_STYLE,
PROP_STYLABLE,
PROP_TRACK_HOVER,
PROP_HOVER,
PROP_CAN_FOCUS,
PROP_LABEL_ACTOR,
PROP_ACCESSIBLE_ROLE
};
enum
{
STYLE_CHANGED,
POPUP_MENU,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0, };
gfloat st_slow_down_factor = 1.0;
G_DEFINE_TYPE (StWidget, st_widget, CLUTTER_TYPE_ACTOR);
#define ST_WIDGET_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ST_TYPE_WIDGET, StWidgetPrivate))
static void st_widget_recompute_style (StWidget *widget,
StThemeNode *old_theme_node);
static gboolean st_widget_real_navigate_focus (StWidget *widget,
ClutterActor *from,
GtkDirectionType direction);
static AtkObject * st_widget_get_accessible (ClutterActor *actor);
static void
st_widget_set_property (GObject *gobject,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
StWidget *actor = ST_WIDGET (gobject);
switch (prop_id)
{
case PROP_THEME:
st_widget_set_theme (actor, g_value_get_object (value));
break;
case PROP_PSEUDO_CLASS:
st_widget_set_style_pseudo_class (actor, g_value_get_string (value));
break;
case PROP_STYLE_CLASS:
st_widget_set_style_class_name (actor, g_value_get_string (value));
break;
case PROP_STYLE:
st_widget_set_style (actor, g_value_get_string (value));
break;
case PROP_STYLABLE:
if (actor->priv->is_stylable != g_value_get_boolean (value))
{
actor->priv->is_stylable = g_value_get_boolean (value);
clutter_actor_queue_relayout ((ClutterActor *) gobject);
}
break;
case PROP_TRACK_HOVER:
st_widget_set_track_hover (actor, g_value_get_boolean (value));
break;
case PROP_HOVER:
st_widget_set_hover (actor, g_value_get_boolean (value));
break;
case PROP_CAN_FOCUS:
st_widget_set_can_focus (actor, g_value_get_boolean (value));
break;
case PROP_LABEL_ACTOR:
st_widget_set_label_actor (actor, g_value_get_object (value));
break;
case PROP_ACCESSIBLE_ROLE:
st_widget_set_accessible_role (actor, g_value_get_enum (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
static void
st_widget_get_property (GObject *gobject,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
StWidget *actor = ST_WIDGET (gobject);
StWidgetPrivate *priv = actor->priv;
switch (prop_id)
{
case PROP_THEME:
g_value_set_object (value, priv->theme);
break;
case PROP_PSEUDO_CLASS:
g_value_set_string (value, priv->pseudo_class);
break;
case PROP_STYLE_CLASS:
g_value_set_string (value, priv->style_class);
break;
case PROP_STYLE:
g_value_set_string (value, priv->inline_style);
break;
case PROP_STYLABLE:
g_value_set_boolean (value, priv->is_stylable);
break;
case PROP_TRACK_HOVER:
g_value_set_boolean (value, priv->track_hover);
break;
case PROP_HOVER:
g_value_set_boolean (value, priv->hover);
break;
case PROP_CAN_FOCUS:
g_value_set_boolean (value, priv->can_focus);
break;
case PROP_LABEL_ACTOR:
g_value_set_object (value, priv->label_actor);
break;
case PROP_ACCESSIBLE_ROLE:
g_value_set_enum (value, st_widget_get_accessible_role (actor));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
static void
st_widget_remove_transition (StWidget *widget)
{
if (widget->priv->transition_animation)
{
g_object_run_dispose (G_OBJECT (widget->priv->transition_animation));
g_object_unref (widget->priv->transition_animation);
widget->priv->transition_animation = NULL;
}
}
static void
st_widget_dispose (GObject *gobject)
{
StWidget *actor = ST_WIDGET (gobject);
StWidgetPrivate *priv = ST_WIDGET (actor)->priv;
if (priv->theme)
{
g_object_unref (priv->theme);
priv->theme = NULL;
}
if (priv->theme_node)
{
g_object_run_dispose (G_OBJECT (priv->theme_node));
g_object_unref (priv->theme_node);
priv->theme_node = NULL;
}
st_widget_remove_transition (actor);
/* The real dispose of this accessible is done on
* AtkGObjectAccessible weak ref callback
*/
if (priv->accessible)
priv->accessible = NULL;
if (priv->label_actor)
{
g_object_unref (priv->label_actor);
priv->label_actor = NULL;
}
g_clear_object (&priv->prev_first_child);
g_clear_object (&priv->prev_last_child);
G_OBJECT_CLASS (st_widget_parent_class)->dispose (gobject);
}
static void
st_widget_finalize (GObject *gobject)
{
StWidgetPrivate *priv = ST_WIDGET (gobject)->priv;
g_free (priv->style_class);
g_free (priv->pseudo_class);
g_object_unref (priv->local_state_set);
G_OBJECT_CLASS (st_widget_parent_class)->finalize (gobject);
}
static void
st_widget_get_preferred_width (ClutterActor *self,
gfloat for_height,
gfloat *min_width_p,
gfloat *natural_width_p)
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
st_theme_node_adjust_for_width (theme_node, &for_height);
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->get_preferred_width (self, for_height, min_width_p, natural_width_p);
st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p);
}
static void
st_widget_get_preferred_height (ClutterActor *self,
gfloat for_width,
gfloat *min_height_p,
gfloat *natural_height_p)
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
st_theme_node_adjust_for_width (theme_node, &for_width);
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->get_preferred_height (self, for_width, min_height_p, natural_height_p);
st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p);
}
static void
st_widget_allocate (ClutterActor *actor,
const ClutterActorBox *box,
ClutterAllocationFlags flags)
{
StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));
ClutterActorBox content_box;
/* Note that we can't just chain up to clutter_actor_real_allocate --
* Clutter does some dirty tricks for backwards compatibility.
* Clutter also passes the actor's allocation directly to the layout
* manager, meaning that we can't modify it for children only.
*/
clutter_actor_set_allocation (actor, box, flags);
st_theme_node_get_content_box (theme_node, box, &content_box);
/* If we've chained up to here, we want to allocate the children using the
* currently installed layout manager */
clutter_layout_manager_allocate (clutter_actor_get_layout_manager (actor),
CLUTTER_CONTAINER (actor),
&content_box,
flags);
}
/**
* st_widget_paint_background:
* @widget: The #StWidget
*
* Paint the background of the widget. This is meant to be called by
* subclasses of StWiget that need to paint the background without
* painting children.
*/
void
st_widget_paint_background (StWidget *widget)
{
StThemeNode *theme_node;
ClutterActorBox allocation;
guint8 opacity;
theme_node = st_widget_get_theme_node (widget);
clutter_actor_get_allocation_box (CLUTTER_ACTOR (widget), &allocation);
opacity = clutter_actor_get_paint_opacity (CLUTTER_ACTOR (widget));
if (widget->priv->transition_animation)
st_theme_node_transition_paint (widget->priv->transition_animation,
&allocation,
opacity);
else
st_theme_node_paint (theme_node, &allocation, opacity);
}
static void
st_widget_paint (ClutterActor *actor)
{
st_widget_paint_background (ST_WIDGET (actor));
/* Chain up so we paint children. */
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->paint (actor);
}
static void
st_widget_parent_set (ClutterActor *widget,
ClutterActor *old_parent)
{
ClutterActorClass *parent_class;
ClutterActor *new_parent;
parent_class = CLUTTER_ACTOR_CLASS (st_widget_parent_class);
if (parent_class->parent_set)
parent_class->parent_set (widget, old_parent);
new_parent = clutter_actor_get_parent (widget);
/* don't send the style changed signal if we no longer have a parent actor */
if (new_parent)
st_widget_style_changed (ST_WIDGET (widget));
}
static void
st_widget_map (ClutterActor *actor)
{
StWidget *self = ST_WIDGET (actor);
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->map (actor);
st_widget_ensure_style (self);
}
static void
st_widget_unmap (ClutterActor *actor)
{
StWidget *self = ST_WIDGET (actor);
StWidgetPrivate *priv = self->priv;
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->unmap (actor);
if (priv->track_hover && priv->hover)
st_widget_set_hover (self, FALSE);
}
static void
notify_children_of_style_change (ClutterActor *self)
{
ClutterActorIter iter;
ClutterActor *actor;
clutter_actor_iter_init (&iter, self);
while (clutter_actor_iter_next (&iter, &actor))
{
if (ST_IS_WIDGET (actor))
st_widget_style_changed (ST_WIDGET (actor));
else
notify_children_of_style_change (actor);
}
}
static void
st_widget_real_style_changed (StWidget *self)
{
StWidgetPrivate *priv = ST_WIDGET (self)->priv;
/* application has request this widget is not stylable */
if (!priv->is_stylable)
return;
clutter_actor_queue_redraw ((ClutterActor *) self);
notify_children_of_style_change ((ClutterActor *) self);
}
void
st_widget_style_changed (StWidget *widget)
{
StThemeNode *old_theme_node = NULL;
widget->priv->is_style_dirty = TRUE;
if (widget->priv->theme_node)
{
old_theme_node = widget->priv->theme_node;
widget->priv->theme_node = NULL;
}
/* update the style only if we are mapped */
if (CLUTTER_ACTOR_IS_MAPPED (CLUTTER_ACTOR (widget)))
st_widget_recompute_style (widget, old_theme_node);
if (old_theme_node)
g_object_unref (old_theme_node);
}
static void
on_theme_context_changed (StThemeContext *context,
ClutterStage *stage)
{
notify_children_of_style_change (CLUTTER_ACTOR (stage));
}
static StThemeNode *
get_root_theme_node (ClutterStage *stage)
{
StThemeContext *context = st_theme_context_get_for_stage (stage);
if (!g_object_get_data (G_OBJECT (context), "st-theme-initialized"))
{
g_object_set_data (G_OBJECT (context), "st-theme-initialized", GUINT_TO_POINTER (1));
g_signal_connect (G_OBJECT (context), "changed",
G_CALLBACK (on_theme_context_changed), stage);
}
return st_theme_context_get_root_node (context);
}
/**
* st_widget_get_theme_node:
* @widget: a #StWidget
*
* Gets the theme node holding style information for the widget.
* The theme node is used to access standard and custom CSS
* properties of the widget.
*
* Note: it is a fatal error to call this on a widget that is
* not been added to a stage.
*
* Return value: (transfer none): the theme node for the widget.
* This is owned by the widget. When attributes of the widget
* or the environment that affect the styling change (for example
* the style_class property of the widget), it will be recreated,
* and the ::style-changed signal will be emitted on the widget.
*/
StThemeNode *
st_widget_get_theme_node (StWidget *widget)
{
StWidgetPrivate *priv = widget->priv;
if (priv->theme_node == NULL)
{
StThemeNode *parent_node = NULL;
ClutterStage *stage = NULL;
ClutterActor *parent;
char *pseudo_class, *direction_pseudo_class;
parent = clutter_actor_get_parent (CLUTTER_ACTOR (widget));
while (parent != NULL)
{
if (parent_node == NULL && ST_IS_WIDGET (parent))
parent_node = st_widget_get_theme_node (ST_WIDGET (parent));
else if (CLUTTER_IS_STAGE (parent))
stage = CLUTTER_STAGE (parent);
parent = clutter_actor_get_parent (parent);
}
if (stage == NULL)
{
g_error ("st_widget_get_theme_node called on the widget %s which is not in the stage.",
st_describe_actor (CLUTTER_ACTOR (widget)));
}
if (parent_node == NULL)
parent_node = get_root_theme_node (CLUTTER_STAGE (stage));
/* Always append a "magic" pseudo class indicating the text
* direction, to allow to adapt the CSS when necessary without
* requiring separate style sheets.
*/
if (clutter_actor_get_text_direction (CLUTTER_ACTOR (widget)) == CLUTTER_TEXT_DIRECTION_RTL)
direction_pseudo_class = "rtl";
else
direction_pseudo_class = "ltr";
if (priv->pseudo_class)
pseudo_class = g_strconcat(priv->pseudo_class, " ",
direction_pseudo_class, NULL);
else
pseudo_class = direction_pseudo_class;
priv->theme_node = st_theme_node_new (st_theme_context_get_for_stage (stage),
parent_node, priv->theme,
G_OBJECT_TYPE (widget),
clutter_actor_get_name (CLUTTER_ACTOR (widget)),
priv->style_class,
pseudo_class,
priv->inline_style);
if (pseudo_class != direction_pseudo_class)
g_free (pseudo_class);
}
return priv->theme_node;
}
/**
* st_widget_peek_theme_node:
* @widget: a #StWidget
*
* Returns the theme node for the widget if it has already been
* computed, %NULL if the widget hasn't been added to a stage or the theme
* node hasn't been computed. If %NULL is returned, then ::style-changed
* will be reliably emitted before the widget is allocated or painted.
*
* Return value: (transfer none): the theme node for the widget.
* This is owned by the widget. When attributes of the widget
* or the environment that affect the styling change (for example
* the style_class property of the widget), it will be recreated,
* and the ::style-changed signal will be emitted on the widget.
*/
StThemeNode *
st_widget_peek_theme_node (StWidget *widget)
{
StWidgetPrivate *priv = widget->priv;
return priv->theme_node;
}
static gboolean
st_widget_enter (ClutterActor *actor,
ClutterCrossingEvent *event)
{
StWidgetPrivate *priv = ST_WIDGET (actor)->priv;
if (priv->track_hover)
{
if (clutter_actor_contains (actor, event->source))
st_widget_set_hover (ST_WIDGET (actor), TRUE);
else
{
/* The widget has a grab and is being told about an
* enter-event outside its hierarchy. Hopefully we already
* got a leave-event, but if not, handle it now.
*/
st_widget_set_hover (ST_WIDGET (actor), FALSE);
}
}
if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->enter_event)
return CLUTTER_ACTOR_CLASS (st_widget_parent_class)->enter_event (actor, event);
else
return FALSE;
}
static gboolean
st_widget_leave (ClutterActor *actor,
ClutterCrossingEvent *event)
{
StWidgetPrivate *priv = ST_WIDGET (actor)->priv;
if (priv->track_hover)
{
if (!event->related || !clutter_actor_contains (actor, event->related))
st_widget_set_hover (ST_WIDGET (actor), FALSE);
}
if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->leave_event)
return CLUTTER_ACTOR_CLASS (st_widget_parent_class)->leave_event (actor, event);
else
return FALSE;
}
static void
st_widget_key_focus_in (ClutterActor *actor)
{
StWidget *widget = ST_WIDGET (actor);
st_widget_add_style_pseudo_class (widget, "focus");
}
static void
st_widget_key_focus_out (ClutterActor *actor)
{
StWidget *widget = ST_WIDGET (actor);
st_widget_remove_style_pseudo_class (widget, "focus");
}
static gboolean
st_widget_key_press_event (ClutterActor *actor,
ClutterKeyEvent *event)
{
if (event->keyval == CLUTTER_KEY_Menu ||
(event->keyval == CLUTTER_KEY_F10 &&
(event->modifier_state & CLUTTER_SHIFT_MASK)))
{
g_signal_emit (actor, signals[POPUP_MENU], 0);
return TRUE;
}
return FALSE;
}
static gboolean
st_widget_get_paint_volume (ClutterActor *self,
ClutterPaintVolume *volume)
{
ClutterActorBox paint_box, alloc_box;
StThemeNode *theme_node;
StWidgetPrivate *priv;
ClutterVertex origin;
/* Setting the paint volume does not make sense when we don't have any allocation */
if (!clutter_actor_has_allocation (self))
return FALSE;
priv = ST_WIDGET (self)->priv;
theme_node = st_widget_get_theme_node (ST_WIDGET (self));
clutter_actor_get_allocation_box (self, &alloc_box);
if (priv->transition_animation)
st_theme_node_transition_get_paint_box (priv->transition_animation,
&alloc_box, &paint_box);
else
st_theme_node_get_paint_box (theme_node, &alloc_box, &paint_box);
origin.x = paint_box.x1 - alloc_box.x1;
origin.y = paint_box.y1 - alloc_box.y1;
origin.z = 0.0f;
clutter_paint_volume_set_origin (volume, &origin);
clutter_paint_volume_set_width (volume, paint_box.x2 - paint_box.x1);
clutter_paint_volume_set_height (volume, paint_box.y2 - paint_box.y1);
if (!clutter_actor_get_clip_to_allocation (self))
{
ClutterActor *child;
/* Based on ClutterGroup/ClutterBox; include the children's
* paint volumes, since they may paint outside our allocation.
*/
for (child = clutter_actor_get_first_child (self);
child != NULL;
child = clutter_actor_get_next_sibling (child))
{
const ClutterPaintVolume *child_volume;
child_volume = clutter_actor_get_transformed_paint_volume (child, self);
if (!child_volume)
return FALSE;
clutter_paint_volume_union (volume, child_volume);
}
}
return TRUE;
}
static GList *
st_widget_real_get_focus_chain (StWidget *widget)
{
GList *children;
GList *focus_chain = NULL;
for (children = clutter_actor_get_children (CLUTTER_ACTOR (widget));
children;
children = children->next)
{
ClutterActor *child = children->data;
if (CLUTTER_ACTOR_IS_VISIBLE (child))
focus_chain = g_list_prepend (focus_chain, child);
}
return g_list_reverse (focus_chain);
}
static void
st_widget_class_init (StWidgetClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
GParamSpec *pspec;
g_type_class_add_private (klass, sizeof (StWidgetPrivate));
gobject_class->set_property = st_widget_set_property;
gobject_class->get_property = st_widget_get_property;
gobject_class->dispose = st_widget_dispose;
gobject_class->finalize = st_widget_finalize;
actor_class->get_preferred_width = st_widget_get_preferred_width;
actor_class->get_preferred_height = st_widget_get_preferred_height;
actor_class->allocate = st_widget_allocate;
actor_class->paint = st_widget_paint;
actor_class->get_paint_volume = st_widget_get_paint_volume;
actor_class->parent_set = st_widget_parent_set;
actor_class->map = st_widget_map;
actor_class->unmap = st_widget_unmap;
actor_class->enter_event = st_widget_enter;
actor_class->leave_event = st_widget_leave;
actor_class->key_focus_in = st_widget_key_focus_in;
actor_class->key_focus_out = st_widget_key_focus_out;
actor_class->key_press_event = st_widget_key_press_event;
actor_class->get_accessible = st_widget_get_accessible;
klass->style_changed = st_widget_real_style_changed;
klass->navigate_focus = st_widget_real_navigate_focus;
klass->get_accessible_type = st_widget_accessible_get_type;
klass->get_focus_chain = st_widget_real_get_focus_chain;
/**
* StWidget:pseudo-class:
*
* The pseudo-class of the actor. Typical values include "hover", "active",
* "focus".
*/
g_object_class_install_property (gobject_class,
PROP_PSEUDO_CLASS,
g_param_spec_string ("pseudo-class",
"Pseudo Class",
"Pseudo class for styling",
"",
ST_PARAM_READWRITE));
/**
* StWidget:style-class:
*
* The style-class of the actor for use in styling.
*/
g_object_class_install_property (gobject_class,
PROP_STYLE_CLASS,
g_param_spec_string ("style-class",
"Style Class",
"Style class for styling",
"",
ST_PARAM_READWRITE));
/**
* StWidget:style:
*
* Inline style information for the actor as a ';'-separated list of
* CSS properties.
*/
g_object_class_install_property (gobject_class,
PROP_STYLE,
g_param_spec_string ("style",
"Style",
"Inline style string",
"",
ST_PARAM_READWRITE));
/**
* StWidget:theme
*
* A theme set on this actor overriding the global theming for this actor
* and its descendants
*/
g_object_class_install_property (gobject_class,
PROP_THEME,
g_param_spec_object ("theme",
"Theme",
"Theme override",
ST_TYPE_THEME,
ST_PARAM_READWRITE));
/**
* StWidget:stylable:
*
* Enable or disable styling of the widget
*/
pspec = g_param_spec_boolean ("stylable",
"Stylable",
"Whether the table should be styled",
TRUE,
ST_PARAM_READWRITE);
g_object_class_install_property (gobject_class,
PROP_STYLABLE,
pspec);
/**
* StWidget:track-hover:
*
* Determines whether the widget tracks pointer hover state. If
* %TRUE (and the widget is visible and reactive), the
* #StWidget:hover property and "hover" style pseudo class will be
* adjusted automatically as the pointer moves in and out of the
* widget.
*/
pspec = g_param_spec_boolean ("track-hover",
"Track hover",
"Determines whether the widget tracks hover state",
FALSE,
ST_PARAM_READWRITE);
g_object_class_install_property (gobject_class,
PROP_TRACK_HOVER,
pspec);
/**
* StWidget:hover:
*
* Whether or not the pointer is currently hovering over the widget. This is
* only tracked automatically if #StWidget:track-hover is %TRUE, but you can
* adjust it manually in any case.
*/
pspec = g_param_spec_boolean ("hover",
"Hover",
"Whether the pointer is hovering over the widget",
FALSE,
ST_PARAM_READWRITE);
g_object_class_install_property (gobject_class,
PROP_HOVER,
pspec);
/**
* StWidget:can-focus:
*
* Whether or not the widget can be focused via keyboard navigation.
*/
pspec = g_param_spec_boolean ("can-focus",
"Can focus",
"Whether the widget can be focused via keyboard navigation",
FALSE,
ST_PARAM_READWRITE);
g_object_class_install_property (gobject_class,
PROP_CAN_FOCUS,
pspec);
/**
* ClutterActor:label-actor:
*
* An actor that labels this widget.
*/
g_object_class_install_property (gobject_class,
PROP_LABEL_ACTOR,
g_param_spec_object ("label-actor",
"Label",
"Label that identifies this widget",
CLUTTER_TYPE_ACTOR,
ST_PARAM_READWRITE));
/**
* StWidget:accessible-role:
*
* The accessible role of this object
*/
g_object_class_install_property (gobject_class,
PROP_ACCESSIBLE_ROLE,
g_param_spec_enum ("accessible-role",
"Accessible Role",
"The accessible role of this object",
ATK_TYPE_ROLE,
ATK_ROLE_INVALID,
G_PARAM_READWRITE));
/**
* StWidget::style-changed:
* @widget: the #StWidget
*
* Emitted when the style information that the widget derives from the
* theme changes
*/
signals[STYLE_CHANGED] =
g_signal_new ("style-changed",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (StWidgetClass, style_changed),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
/**
* StWidget::popup-menu:
* @widget: the #StWidget
*
* Emitted when the user has requested a context menu (eg, via a
* keybinding)
*/
signals[POPUP_MENU] =
g_signal_new ("popup-menu",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (StWidgetClass, popup_menu),
NULL, NULL, NULL,
G_TYPE_NONE, 0);
}
/**
* st_widget_set_theme:
* @actor: a #StWidget
* @theme: a new style class string
*
* Overrides the theme that would be inherited from the actor's parent
* or the stage with an entirely new theme (set of stylesheets).
*/
void
st_widget_set_theme (StWidget *actor,
StTheme *theme)
{
StWidgetPrivate *priv = actor->priv;
g_return_if_fail (ST_IS_WIDGET (actor));
priv = actor->priv;
if (theme !=priv->theme)
{
if (priv->theme)
g_object_unref (priv->theme);
priv->theme = g_object_ref (priv->theme);
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "theme");
}
}
/**
* st_widget_get_theme:
* @actor: a #StWidget
*
* Gets the overriding theme set on the actor. See st_widget_set_theme()
*
* Return value: (transfer none): the overriding theme, or %NULL
*/
StTheme *
st_widget_get_theme (StWidget *actor)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
return actor->priv->theme;
}
static const gchar *
find_class_name (const gchar *class_list,
const gchar *class_name)
{
gint len = strlen (class_name);
const gchar *match;
if (!class_list)
return NULL;
for (match = strstr (class_list, class_name); match; match = strstr (match + 1, class_name))
{
if ((match == class_list || g_ascii_isspace (match[-1])) &&
(match[len] == '\0' || g_ascii_isspace (match[len])))
return match;
}
return NULL;
}
static gboolean
set_class_list (gchar **class_list,
const gchar *new_class_list)
{
if (g_strcmp0 (*class_list, new_class_list) != 0)
{
g_free (*class_list);
*class_list = g_strdup (new_class_list);
return TRUE;
}
else
return FALSE;
}
static gboolean
add_class_name (gchar **class_list,
const gchar *class_name)
{
gchar *new_class_list;
if (*class_list)
{
if (find_class_name (*class_list, class_name))
return FALSE;
new_class_list = g_strdup_printf ("%s %s", *class_list, class_name);
g_free (*class_list);
*class_list = new_class_list;
}
else
*class_list = g_strdup (class_name);
return TRUE;
}
static gboolean
remove_class_name (gchar **class_list,
const gchar *class_name)
{
const gchar *match, *end;
gchar *new_class_list;
if (!*class_list)
return FALSE;
if (strcmp (*class_list, class_name) == 0)
{
g_free (*class_list);
*class_list = NULL;
return TRUE;
}
match = find_class_name (*class_list, class_name);
if (!match)
return FALSE;
end = match + strlen (class_name);
/* Adjust either match or end to include a space as well.
* (One or the other must be possible at this point.)
*/
if (match != *class_list)
match--;
else
end++;
new_class_list = g_strdup_printf ("%.*s%s", (int)(match - *class_list),
*class_list, end);
g_free (*class_list);
*class_list = new_class_list;
return TRUE;
}
/**
* st_widget_set_style_class_name:
* @actor: a #StWidget
* @style_class_list: (allow-none): a new style class list string
*
* Set the style class name list. @style_class_list can either be
* %NULL, for no classes, or a space-separated list of style class
* names. See also st_widget_add_style_class_name() and
* st_widget_remove_style_class_name().
*/
void
st_widget_set_style_class_name (StWidget *actor,
const gchar *style_class_list)
{
g_return_if_fail (ST_IS_WIDGET (actor));
if (set_class_list (&actor->priv->style_class, style_class_list))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "style-class");
}
}
/**
* st_widget_add_style_class_name:
* @actor: a #StWidget
* @style_class: a style class name string
*
* Adds @style_class to @actor's style class name list, if it is not
* already present.
*/
void
st_widget_add_style_class_name (StWidget *actor,
const gchar *style_class)
{
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (style_class != NULL);
if (add_class_name (&actor->priv->style_class, style_class))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "style-class");
}
}
/**
* st_widget_remove_style_class_name:
* @actor: a #StWidget
* @style_class: a style class name string
*
* Removes @style_class from @actor's style class name, if it is
* present.
*/
void
st_widget_remove_style_class_name (StWidget *actor,
const gchar *style_class)
{
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (style_class != NULL);
if (remove_class_name (&actor->priv->style_class, style_class))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "style-class");
}
}
/**
* st_widget_get_style_class_name:
* @actor: a #StWidget
*
* Get the current style class name
*
* Returns: the class name string. The string is owned by the #StWidget and
* should not be modified or freed.
*/
const gchar*
st_widget_get_style_class_name (StWidget *actor)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
return actor->priv->style_class;
}
/**
* st_widget_has_style_class_name:
* @actor: a #StWidget
* @style_class: a style class string
*
* Tests if @actor's style class list includes @style_class.
*
* Returns: whether or not @actor's style class list includes
* @style_class.
*/
gboolean
st_widget_has_style_class_name (StWidget *actor,
const gchar *style_class)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE);
return find_class_name (actor->priv->style_class, style_class) != NULL;
}
/**
* st_widget_get_style_pseudo_class:
* @actor: a #StWidget
*
* Get the current style pseudo class list.
*
* Note that an actor can have multiple pseudo classes; if you just
* want to test for the presence of a specific pseudo class, use
* st_widget_has_style_pseudo_class().
*
* Returns: the pseudo class list string. The string is owned by the
* #StWidget and should not be modified or freed.
*/
const gchar*
st_widget_get_style_pseudo_class (StWidget *actor)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
return actor->priv->pseudo_class;
}
/**
* st_widget_has_style_pseudo_class:
* @actor: a #StWidget
* @pseudo_class: a pseudo class string
*
* Tests if @actor's pseudo class list includes @pseudo_class.
*
* Returns: whether or not @actor's pseudo class list includes
* @pseudo_class.
*/
gboolean
st_widget_has_style_pseudo_class (StWidget *actor,
const gchar *pseudo_class)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE);
return find_class_name (actor->priv->pseudo_class, pseudo_class) != NULL;
}
/**
* st_widget_set_style_pseudo_class:
* @actor: a #StWidget
* @pseudo_class_list: (allow-none): a new pseudo class list string
*
* Set the style pseudo class list. @pseudo_class_list can either be
* %NULL, for no classes, or a space-separated list of pseudo class
* names. See also st_widget_add_style_pseudo_class() and
* st_widget_remove_style_pseudo_class().
*/
void
st_widget_set_style_pseudo_class (StWidget *actor,
const gchar *pseudo_class_list)
{
g_return_if_fail (ST_IS_WIDGET (actor));
if (set_class_list (&actor->priv->pseudo_class, pseudo_class_list))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "pseudo-class");
}
}
/**
* st_widget_add_style_pseudo_class:
* @actor: a #StWidget
* @pseudo_class: a pseudo class string
*
* Adds @pseudo_class to @actor's pseudo class list, if it is not
* already present.
*/
void
st_widget_add_style_pseudo_class (StWidget *actor,
const gchar *pseudo_class)
{
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (pseudo_class != NULL);
if (add_class_name (&actor->priv->pseudo_class, pseudo_class))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "pseudo-class");
}
}
/**
* st_widget_remove_style_pseudo_class:
* @actor: a #StWidget
* @pseudo_class: a pseudo class string
*
* Removes @pseudo_class from @actor's pseudo class, if it is present.
*/
void
st_widget_remove_style_pseudo_class (StWidget *actor,
const gchar *pseudo_class)
{
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (pseudo_class != NULL);
if (remove_class_name (&actor->priv->pseudo_class, pseudo_class))
{
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "pseudo-class");
}
}
/**
* st_widget_set_style:
* @actor: a #StWidget
* @style: (allow-none): a inline style string, or %NULL
*
* Set the inline style string for this widget. The inline style string is an
* optional ';'-separated list of CSS properties that override the style as
* determined from the stylesheets of the current theme.
*/
void
st_widget_set_style (StWidget *actor,
const gchar *style)
{
StWidgetPrivate *priv = actor->priv;
g_return_if_fail (ST_IS_WIDGET (actor));
priv = actor->priv;
if (g_strcmp0 (style, priv->inline_style))
{
g_free (priv->inline_style);
priv->inline_style = g_strdup (style);
st_widget_style_changed (actor);
g_object_notify (G_OBJECT (actor), "style");
}
}
/**
* st_widget_get_style:
* @actor: a #StWidget
*
* Get the current inline style string. See st_widget_set_style().
*
* Returns: The inline style string, or %NULL. The string is owned by the
* #StWidget and should not be modified or freed.
*/
const gchar*
st_widget_get_style (StWidget *actor)
{
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
return actor->priv->inline_style;
}
static void
st_widget_name_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
st_widget_style_changed (widget);
}
static void
st_widget_first_child_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
ClutterActor *first_child;
if (widget->priv->prev_first_child != NULL)
{
st_widget_remove_style_pseudo_class (widget->priv->prev_first_child, "first-child");
g_clear_object (&widget->priv->prev_first_child);
}
first_child = clutter_actor_get_first_child (CLUTTER_ACTOR (widget));
if (first_child == NULL)
return;
if (ST_IS_WIDGET (first_child))
{
st_widget_add_style_pseudo_class (ST_WIDGET (first_child), "first-child");
widget->priv->prev_first_child = g_object_ref (ST_WIDGET (first_child));
}
}
static void
st_widget_last_child_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
ClutterActor *last_child;
if (widget->priv->prev_last_child != NULL)
{
st_widget_remove_style_pseudo_class (widget->priv->prev_last_child, "last-child");
g_clear_object (&widget->priv->prev_last_child);
}
last_child = clutter_actor_get_last_child (CLUTTER_ACTOR (widget));
if (last_child == NULL)
return;
if (ST_IS_WIDGET (last_child))
{
st_widget_add_style_pseudo_class (ST_WIDGET (last_child), "last-child");
widget->priv->prev_last_child = g_object_ref (ST_WIDGET (last_child));
}
}
static void
st_widget_init (StWidget *actor)
{
StWidgetPrivate *priv;
actor->priv = priv = ST_WIDGET_GET_PRIVATE (actor);
priv->is_stylable = TRUE;
priv->transition_animation = NULL;
priv->local_state_set = atk_state_set_new ();
/* connect style changed */
g_signal_connect (actor, "notify::name", G_CALLBACK (st_widget_name_notify), NULL);
g_signal_connect (actor, "notify::first-child", G_CALLBACK (st_widget_first_child_notify), NULL);
g_signal_connect (actor, "notify::last-child", G_CALLBACK (st_widget_last_child_notify), NULL);
}
static void
on_transition_completed (StThemeNodeTransition *transition,
StWidget *widget)
{
st_widget_remove_transition (widget);
}
static void
st_widget_recompute_style (StWidget *widget,
StThemeNode *old_theme_node)
{
StThemeNode *new_theme_node = st_widget_get_theme_node (widget);
int transition_duration;
gboolean paint_equal;
if (!old_theme_node ||
!st_theme_node_geometry_equal (old_theme_node, new_theme_node))
clutter_actor_queue_relayout ((ClutterActor *) widget);
transition_duration = st_theme_node_get_transition_duration (new_theme_node);
paint_equal = old_theme_node && st_theme_node_paint_equal (old_theme_node, new_theme_node);
if (paint_equal)
st_theme_node_copy_cached_paint_state (new_theme_node, old_theme_node);
if (transition_duration > 0)
{
if (widget->priv->transition_animation != NULL)
{
st_theme_node_transition_update (widget->priv->transition_animation,
new_theme_node);
}
else if (old_theme_node && !paint_equal)
{
/* Since our transitions are only of the painting done by StThemeNode, we
* only want to start a transition when what is painted changes; if
* other visual aspects like the foreground color of a label change,
* we can't animate that anyways.
*/
widget->priv->transition_animation =
st_theme_node_transition_new (old_theme_node,
new_theme_node,
transition_duration);
g_signal_connect (widget->priv->transition_animation, "completed",
G_CALLBACK (on_transition_completed), widget);
g_signal_connect_swapped (widget->priv->transition_animation,
"new-frame",
G_CALLBACK (clutter_actor_queue_redraw),
widget);
}
}
else if (widget->priv->transition_animation)
{
st_widget_remove_transition (widget);
}
g_signal_emit (widget, signals[STYLE_CHANGED], 0);
widget->priv->is_style_dirty = FALSE;
}
/**
* st_widget_ensure_style:
* @widget: A #StWidget
*
* Ensures that @widget has read its style information.
*
*/
void
st_widget_ensure_style (StWidget *widget)
{
g_return_if_fail (ST_IS_WIDGET (widget));
if (widget->priv->is_style_dirty)
st_widget_recompute_style (widget, NULL);
}
/**
* st_widget_set_track_hover:
* @widget: A #StWidget
* @track_hover: %TRUE if the widget should track the pointer hover state
*
* Enables hover tracking on the #StWidget.
*
* If hover tracking is enabled, and the widget is visible and
* reactive, then @widget's #StWidget:hover property will be updated
* automatically to reflect whether the pointer is in @widget (or one
* of its children), and @widget's #StWidget:pseudo-class will have
* the "hover" class added and removed from it accordingly.
*
* Note that currently it is not possible to correctly track the hover
* state when another actor has a pointer grab. You can use
* st_widget_sync_hover() to update the property manually in this
* case.
*/
void
st_widget_set_track_hover (StWidget *widget,
gboolean track_hover)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = widget->priv;
if (priv->track_hover != track_hover)
{
priv->track_hover = track_hover;
g_object_notify (G_OBJECT (widget), "track-hover");
if (priv->track_hover)
st_widget_sync_hover (widget);
}
}
/**
* st_widget_get_track_hover:
* @widget: A #StWidget
*
* Returns the current value of the track-hover property. See
* st_widget_set_track_hover() for more information.
*
* Returns: current value of track-hover on @widget
*/
gboolean
st_widget_get_track_hover (StWidget *widget)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE);
return widget->priv->track_hover;
}
/**
* st_widget_set_hover:
* @widget: A #StWidget
* @hover: whether the pointer is hovering over the widget
*
* Sets @widget's hover property and adds or removes "hover" from its
* pseudo class accordingly.
*
* If you have set #StWidget:track-hover, you should not need to call
* this directly. You can call st_widget_sync_hover() if the hover
* state might be out of sync due to another actor's pointer grab.
*/
void
st_widget_set_hover (StWidget *widget,
gboolean hover)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = widget->priv;
if (priv->hover != hover)
{
priv->hover = hover;
if (priv->hover)
st_widget_add_style_pseudo_class (widget, "hover");
else
st_widget_remove_style_pseudo_class (widget, "hover");
g_object_notify (G_OBJECT (widget), "hover");
}
}
/**
* st_widget_sync_hover:
* @widget: A #StWidget
*
* Sets @widget's hover state according to the current pointer
* position. This can be used to ensure that it is correct after
* (or during) a pointer grab.
*/
void
st_widget_sync_hover (StWidget *widget)
{
ClutterDeviceManager *device_manager;
ClutterInputDevice *pointer;
ClutterActor *pointer_actor;
device_manager = clutter_device_manager_get_default ();
pointer = clutter_device_manager_get_core_device (device_manager,
CLUTTER_POINTER_DEVICE);
pointer_actor = clutter_input_device_get_pointer_actor (pointer);
if (pointer_actor)
st_widget_set_hover (widget, clutter_actor_contains (CLUTTER_ACTOR (widget), pointer_actor));
else
st_widget_set_hover (widget, FALSE);
}
/**
* st_widget_get_hover:
* @widget: A #StWidget
*
* If #StWidget:track-hover is set, this returns whether the pointer
* is currently over the widget.
*
* Returns: current value of hover on @widget
*/
gboolean
st_widget_get_hover (StWidget *widget)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE);
return widget->priv->hover;
}
/**
* st_widget_set_can_focus:
* @widget: A #StWidget
* @can_focus: %TRUE if the widget can receive keyboard focus
* via keyboard navigation
*
* Marks @widget as being able to receive keyboard focus via
* keyboard navigation.
*/
void
st_widget_set_can_focus (StWidget *widget,
gboolean can_focus)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = widget->priv;
if (priv->can_focus != can_focus)
{
priv->can_focus = can_focus;
g_object_notify (G_OBJECT (widget), "can-focus");
}
}
/**
* st_widget_get_can_focus:
* @widget: A #StWidget
*
* Returns the current value of the can-focus property. See
* st_widget_set_can_focus() for more information.
*
* Returns: current value of can-focus on @widget
*/
gboolean
st_widget_get_can_focus (StWidget *widget)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE);
return widget->priv->can_focus;
}
/* 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;
ClutterVertex abs_vertices[4];
GList *l, *ret;
ClutterActor *child;
for (l = children, ret = NULL; l; l = l->next)
{
child = l->data;
clutter_actor_get_abs_allocation_vertices (child, abs_vertices);
clutter_actor_box_from_vertices (&cbox, abs_vertices);
/* Filter out children if they are in the wrong direction from
* @rbox, or if they don't overlap it. To account for floating-
* point imprecision, an actor is "down" (etc.) from an another
* actor even if it overlaps it by up to 0.1 pixels.
*/
switch (direction)
{
case GTK_DIR_UP:
if (cbox.y2 > rbox->y1 + 0.1)
continue;
break;
case GTK_DIR_DOWN:
if (cbox.y1 < rbox->y2 - 0.1)
continue;
break;
case GTK_DIR_LEFT:
if (cbox.x2 > rbox->x1 + 0.1)
continue;
break;
case GTK_DIR_RIGHT:
if (cbox.x1 < rbox->x2 - 0.1)
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;
} StWidgetChildSortData;
static int
sort_by_position (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
ClutterActor *actor_a = (ClutterActor *)a;
ClutterActor *actor_b = (ClutterActor *)b;
StWidgetChildSortData *sort_data = user_data;
GtkDirectionType direction = sort_data->direction;
ClutterActorBox abox, bbox;
ClutterVertex abs_vertices[4];
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_abs_allocation_vertices (actor_a, abs_vertices);
clutter_actor_box_from_vertices (&abox, abs_vertices);
ax = (int)(abox.x1 + abox.x2) / 2;
ay = (int)(abox.y1 + abox.y2) / 2;
clutter_actor_get_abs_allocation_vertices (actor_b, abs_vertices);
clutter_actor_box_from_vertices (&bbox, abs_vertices);
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_widget_real_navigate_focus (StWidget *widget,
ClutterActor *from,
GtkDirectionType direction)
{
ClutterActor *widget_actor, *focus_child;
GList *children, *l;
widget_actor = CLUTTER_ACTOR (widget);
if (from == widget_actor)
return FALSE;
/* Figure out if @from is a descendant of @widget, and if so,
* set @focus_child to the immediate child of @widget that
* contains (or *is*) @from.
*/
focus_child = from;
while (focus_child && clutter_actor_get_parent (focus_child) != widget_actor)
focus_child = clutter_actor_get_parent (focus_child);
if (widget->priv->can_focus)
{
if (!focus_child)
{
/* Accept focus from outside */
clutter_actor_grab_key_focus (widget_actor);
return TRUE;
}
else
{
/* Yield focus from within: since @widget itself is
* focusable we don't allow the focus to be navigated
* within @widget.
*/
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;
}
children = st_widget_get_focus_chain (widget);
if (direction == GTK_DIR_TAB_FORWARD ||
direction == GTK_DIR_TAB_BACKWARD)
{
/* At this point we know that we want to navigate focus to one of
* @widget'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.)
*/
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 */
{
StWidgetChildSortData sort_data;
ClutterVertex abs_vertices[4];
/* Compute the allocation box of the previous focused actor. If there
* was no previous focus, use the coordinates of the appropriate edge of
* @widget.
*
* Note that all of this code assumes the actors are not
* transformed (or at most, they are all scaled by the same
* amount). If @widget or any of its children is rotated, or
* any child is inconsistently scaled, then the focus chain will
* probably be unpredictable.
*/
if (from)
{
clutter_actor_get_abs_allocation_vertices (from, abs_vertices);
clutter_actor_box_from_vertices (&sort_data.box, abs_vertices);
}
else
{
clutter_actor_get_abs_allocation_vertices (widget_actor, abs_vertices);
clutter_actor_box_from_vertices (&sort_data.box, abs_vertices);
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 (from)
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;
}
/**
* st_widget_navigate_focus:
* @widget: the "top level" container
* @from: (allow-none): the actor that the focus is coming from
* @direction: the direction focus is moving in
* @wrap_around: whether focus should wrap around
*
* Tries to update the keyboard focus within @widget in response to a
* keyboard event.
*
* If @from is a descendant of @widget, this attempts to move the
* keyboard focus to the next descendant of @widget (in the order
* implied by @direction) that has the #StWidget:can-focus property
* set. If @from is %NULL, this attempts to focus either @widget
* itself, or its first descendant in the order implied by
* @direction. If @from is outside of @widget, it behaves as if it was
* a descendant if @direction is one of the directional arrows and as
* if it was %NULL otherwise.
*
* If a container type is marked #StWidget:can-focus, the expected
* behavior is that it will only take up a single slot on the focus
* chain as a whole, rather than allowing navigation between its child
* actors (or having a distinction between itself being focused and
* one of its children being focused).
*
* Some widget classes might have slightly different behavior from the
* above, where that would make more sense.
*
* If @wrap_around is %TRUE and @from is a child of @widget, but the
* widget has no further children that can accept the focus in the
* given direction, then st_widget_navigate_focus() will try a second
* time, using a %NULL @from, which should cause it to reset the focus
* to the first available widget in the given direction.
*
* Return value: %TRUE if clutter_actor_grab_key_focus() has been
* called on an actor. %FALSE if not.
*/
gboolean
st_widget_navigate_focus (StWidget *widget,
ClutterActor *from,
GtkDirectionType direction,
gboolean wrap_around)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), FALSE);
if (ST_WIDGET_GET_CLASS (widget)->navigate_focus (widget, from, direction))
return TRUE;
if (wrap_around && from && clutter_actor_contains (CLUTTER_ACTOR (widget), from))
return ST_WIDGET_GET_CLASS (widget)->navigate_focus (widget, NULL, direction);
return FALSE;
}
static gboolean
append_actor_text (GString *desc,
ClutterActor *actor)
{
if (CLUTTER_IS_TEXT (actor))
{
g_string_append_printf (desc, " (\"%s\")",
clutter_text_get_text (CLUTTER_TEXT (actor)));
return TRUE;
}
else if (ST_IS_LABEL (actor))
{
g_string_append_printf (desc, " (\"%s\")",
st_label_get_text (ST_LABEL (actor)));
return TRUE;
}
else
return FALSE;
}
/**
* st_describe_actor:
* @actor: a #ClutterActor
*
* Creates a string describing @actor, for use in debugging. This
* includes the class name and actor name (if any), plus if @actor
* is an #StWidget, its style class and pseudo class names.
*
* Return value: the debug name.
*/
char *
st_describe_actor (ClutterActor *actor)
{
GString *desc;
const char *name;
int i;
if (!actor)
return g_strdup ("[null]");
desc = g_string_new (NULL);
g_string_append_printf (desc, "[%p %s", actor,
G_OBJECT_TYPE_NAME (actor));
if (ST_IS_WIDGET (actor))
{
const char *style_class = st_widget_get_style_class_name (ST_WIDGET (actor));
const char *pseudo_class = st_widget_get_style_pseudo_class (ST_WIDGET (actor));
char **classes;
if (style_class)
{
classes = g_strsplit (style_class, ",", -1);
for (i = 0; classes[i]; i++)
{
g_strchug (classes[i]);
g_string_append_printf (desc, ".%s", classes[i]);
}
g_strfreev (classes);
}
if (pseudo_class)
{
classes = g_strsplit (pseudo_class, ",", -1);
for (i = 0; classes[i]; i++)
{
g_strchug (classes[i]);
g_string_append_printf (desc, ":%s", classes[i]);
}
g_strfreev (classes);
}
}
name = clutter_actor_get_name (actor);
if (name)
g_string_append_printf (desc, " \"%s\"", name);
if (!append_actor_text (desc, actor))
{
GList *children, *l;
/* Do a limited search of @actor's children looking for a label */
children = clutter_actor_get_children (actor);
for (l = children, i = 0; l && i < 20; l = l->next, i++)
{
if (append_actor_text (desc, l->data))
break;
children = g_list_concat (children, clutter_actor_get_children (l->data));
}
g_list_free (children);
}
g_string_append_c (desc, ']');
return g_string_free (desc, FALSE);
}
/**
* st_set_slow_down_factor:
*
* @factor: new slow-down factor
*
* Set a global factor applied to all animation durations
*/
void
st_set_slow_down_factor (gfloat factor)
{
st_slow_down_factor = factor;
}
/**
* st_get_slow_down_factor:
*
* Returns: the global factor applied to all animation durations
*/
gfloat
st_get_slow_down_factor ()
{
return st_slow_down_factor;
}
/**
* st_widget_get_label_actor:
* @widget: a #StWidget
*
* Gets the label that identifies @widget if it is defined
*
* Return value: (transfer none): the label that identifies the widget
*/
ClutterActor *
st_widget_get_label_actor (StWidget *widget)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), NULL);
return widget->priv->label_actor;
}
/**
* st_widget_set_label_actor:
* @widget: a #StWidget
* @label: a #ClutterActor
*
* Sets @label as the #ClutterActor that identifies (labels)
* @widget. @label can be %NULL to indicate that @widget is not
* labelled any more
*/
void
st_widget_set_label_actor (StWidget *widget,
ClutterActor *label)
{
g_return_if_fail (ST_IS_WIDGET (widget));
if (widget->priv->label_actor != label)
{
if (widget->priv->label_actor)
g_object_unref (widget->priv->label_actor);
if (label != NULL)
widget->priv->label_actor = g_object_ref (label);
else
widget->priv->label_actor = NULL;
g_object_notify (G_OBJECT (widget), "label-actor");
}
}
/**
* st_widget_set_accessible_role:
* @widget: widget to set the accessible role for
* @role: The role to use
*
* This method sets @role as the accessible role for @widget. This
* role describes what kind of user interface element @widget is and
* is provided so that assistive technologies know how to present
* @widget to the user.
*
* Usually you will have no need to set the accessible role for an
* object, as this information is extracted from the context of the
* object (ie: a #StButton has by default a push button role). This
* method is only required when you need to redefine the role
* currently associated with the widget, for instance if it is being
* used in an unusual way (ie: a #StButton used as a togglebutton), or
* if a generic object is used directly (ie: a container as a menu
* item).
*
* If @role is #ATK_ROLE_INVALID, the role will not be changed
* and the accessible's default role will be used instead.
*/
void
st_widget_set_accessible_role (StWidget *widget,
AtkRole role)
{
g_return_if_fail (ST_IS_WIDGET (widget));
widget->priv->accessible_role = role;
g_object_notify (G_OBJECT (widget), "accessible-role");
}
/**
* st_widget_get_accessible_role:
* @widget: widget to get the accessible role for
*
* Gets the #AtkRole for this widget. See
* st_widget_set_accessible_role() for more information.
*
* Return value: accessible #AtkRole for this widget
*/
AtkRole
st_widget_get_accessible_role (StWidget *widget)
{
AtkObject *accessible = NULL;
AtkRole role = ATK_ROLE_INVALID;
g_return_val_if_fail (ST_IS_WIDGET (widget), ATK_ROLE_INVALID);
if (widget->priv->accessible_role != ATK_ROLE_INVALID)
role = widget->priv->accessible_role;
else if (widget->priv->accessible != NULL)
role = atk_object_get_role (accessible);
return role;
}
static void
notify_accessible_state_change (StWidget *widget,
AtkStateType state,
gboolean value)
{
if (widget->priv->accessible != NULL)
atk_object_notify_state_change (widget->priv->accessible, state, value);
}
/**
* st_widget_add_accessible_state:
* @widget: A #StWidget
* @state: #AtkStateType state to add
*
* This method adds @state as one of the accessible states for
* @widget. The list of states of a widget describes the current state
* of user interface element @widget and is provided so that assistive
* technologies know how to present @widget to the user.
*
* Usually you will have no need to add accessible states for an
* object, as the accessible object can extract most of the states
* from the object itself (ie: a #StButton knows when it is pressed).
* This method is only required when one cannot extract the
* information automatically from the object itself (i.e.: a generic
* container used as a toggle menu item will not automatically include
* the toggled state).
*
*/
void
st_widget_add_accessible_state (StWidget *widget,
AtkStateType state)
{
g_return_if_fail (ST_IS_WIDGET (widget));
if (atk_state_set_add_state (widget->priv->local_state_set, state))
notify_accessible_state_change (widget, state, TRUE);
}
/**
* st_widget_remove_accessible_state:
* @widget: A #StWidget
* @state: #AtkState state to remove
*
* This method removes @state as on of the accessible states for
* @widget. See st_widget_add_accessible_state() for more information.
*
*/
void
st_widget_remove_accessible_state (StWidget *widget,
AtkStateType state)
{
g_return_if_fail (ST_IS_WIDGET (widget));
if (atk_state_set_remove_state (widget->priv->local_state_set, state))
notify_accessible_state_change (widget, state, FALSE);
}
/******************************************************************************/
/*************************** ACCESSIBILITY SUPPORT ****************************/
/******************************************************************************/
/* GObject */
static void st_widget_accessible_class_init (StWidgetAccessibleClass *klass);
static void st_widget_accessible_init (StWidgetAccessible *widget);
static void st_widget_accessible_dispose (GObject *gobject);
/* AtkObject */
static AtkStateSet *st_widget_accessible_ref_state_set (AtkObject *obj);
static void st_widget_accessible_initialize (AtkObject *obj,
gpointer data);
static AtkRole st_widget_accessible_get_role (AtkObject *obj);
/* Private methods */
static void on_pseudo_class_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data);
static void on_can_focus_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data);
static void on_label_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data);
static void check_pseudo_class (StWidgetAccessible *self,
StWidget *widget);
static void check_labels (StWidgetAccessible *self,
StWidget *widget);
G_DEFINE_TYPE (StWidgetAccessible, st_widget_accessible, CALLY_TYPE_ACTOR)
#define ST_WIDGET_ACCESSIBLE_GET_PRIVATE(obj) \
(G_TYPE_INSTANCE_GET_PRIVATE ((obj), ST_TYPE_WIDGET_ACCESSIBLE, \
StWidgetAccessiblePrivate))
struct _StWidgetAccessiblePrivate
{
/* Cached values (used to avoid extra notifications) */
gboolean selected;
gboolean checked;
/* The current_label. Right now there are the proper atk
* relationships between this object and the label
*/
AtkObject *current_label;
};
static AtkObject *
st_widget_get_accessible (ClutterActor *actor)
{
StWidget *widget = NULL;
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
widget = ST_WIDGET (actor);
if (widget->priv->accessible == NULL)
{
widget->priv->accessible =
g_object_new (ST_WIDGET_GET_CLASS (widget)->get_accessible_type (),
NULL);
atk_object_initialize (widget->priv->accessible, actor);
}
return widget->priv->accessible;
}
static void
st_widget_accessible_class_init (StWidgetAccessibleClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
AtkObjectClass *atk_class = ATK_OBJECT_CLASS (klass);
gobject_class->dispose = st_widget_accessible_dispose;
atk_class->ref_state_set = st_widget_accessible_ref_state_set;
atk_class->initialize = st_widget_accessible_initialize;
atk_class->get_role = st_widget_accessible_get_role;
g_type_class_add_private (gobject_class, sizeof (StWidgetAccessiblePrivate));
}
static void
st_widget_accessible_init (StWidgetAccessible *self)
{
StWidgetAccessiblePrivate *priv = ST_WIDGET_ACCESSIBLE_GET_PRIVATE (self);
self->priv = priv;
}
static void
st_widget_accessible_dispose (GObject *gobject)
{
StWidgetAccessible *self = ST_WIDGET_ACCESSIBLE (gobject);
if (self->priv->current_label)
{
g_object_unref (self->priv->current_label);
self->priv->current_label = NULL;
}
}
static void
st_widget_accessible_initialize (AtkObject *obj,
gpointer data)
{
ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->initialize (obj, data);
g_signal_connect (data, "notify::pseudo-class",
G_CALLBACK (on_pseudo_class_notify),
obj);
g_signal_connect (data, "notify::can-focus",
G_CALLBACK (on_can_focus_notify),
obj);
g_signal_connect (data, "notify::label-actor",
G_CALLBACK (on_label_notify),
obj);
/* Check the cached selected state and notify the first selection.
* Ie: it is required to ensure a first notification when Alt+Tab
* popup appears
*/
check_pseudo_class (ST_WIDGET_ACCESSIBLE (obj), ST_WIDGET (data));
check_labels (ST_WIDGET_ACCESSIBLE (obj), ST_WIDGET (data));
}
static AtkStateSet *
st_widget_accessible_ref_state_set (AtkObject *obj)
{
AtkStateSet *result = NULL;
AtkStateSet *aux_set = NULL;
ClutterActor *actor = NULL;
StWidget *widget = NULL;
StWidgetAccessible *self = NULL;
result = ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->ref_state_set (obj);
actor = CLUTTER_ACTOR (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj)));
if (actor == NULL) /* State is defunct */
return result;
widget = ST_WIDGET (actor);
self = ST_WIDGET_ACCESSIBLE (obj);
/* priv->selected should be properly updated on the
* ATK_STATE_SELECTED notification callbacks
*/
if (self->priv->selected)
atk_state_set_add_state (result, ATK_STATE_SELECTED);
if (self->priv->checked)
atk_state_set_add_state (result, ATK_STATE_CHECKED);
/* On clutter there isn't any tip to know if a actor is focusable or
* not, anyone can receive the key_focus. For this reason
* cally_actor sets any actor as FOCUSABLE. This is not the case on
* St, where we have can_focus. But this means that we need to
* remove the state FOCUSABLE if it is not focusable
*/
if (st_widget_get_can_focus (widget))
atk_state_set_add_state (result, ATK_STATE_FOCUSABLE);
else
atk_state_set_remove_state (result, ATK_STATE_FOCUSABLE);
/* We add the states added externally if required */
if (!atk_state_set_is_empty (widget->priv->local_state_set))
{
aux_set = atk_state_set_or_sets (result, widget->priv->local_state_set);
g_object_unref (result); /* previous result will not be used */
result = aux_set;
}
return result;
}
static AtkRole
st_widget_accessible_get_role (AtkObject *obj)
{
StWidget *widget = NULL;
g_return_val_if_fail (ST_IS_WIDGET_ACCESSIBLE (obj), ATK_ROLE_INVALID);
widget = ST_WIDGET (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj)));
if (widget == NULL)
return ATK_ROLE_INVALID;
if (widget->priv->accessible_role != ATK_ROLE_INVALID)
return widget->priv->accessible_role;
return ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->get_role (obj);
}
static void
on_pseudo_class_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data)
{
check_pseudo_class (ST_WIDGET_ACCESSIBLE (data),
ST_WIDGET (gobject));
}
/*
* In some cases the only way to check some states are checking the
* pseudo-class. Like if the object is selected (see bug 637830) or if
* the object is toggled. This method also notifies a state change if
* the value is different to the one cached.
*
* We also assume that if the object uses that pseudo-class, it makes
* sense to notify that state change. It would be possible to refine
* that behaviour checking the role (ie: notify CHECKED changes only
* for CHECK_BUTTON roles).
*
* In a ideal world we would have a more standard way to get the
* state, like the widget-context (as in the case of
* gtktreeview-cells), or something like the property "can-focus". But
* for the moment this is enough, and we can update that in the future
* if required.
*/
static void
check_pseudo_class (StWidgetAccessible *self,
StWidget *widget)
{
gboolean found = FALSE;
found = st_widget_has_style_pseudo_class (widget,
"selected");
if (found != self->priv->selected)
{
self->priv->selected = found;
atk_object_notify_state_change (ATK_OBJECT (self),
ATK_STATE_SELECTED,
found);
}
found = st_widget_has_style_pseudo_class (widget,
"checked");
if (found != self->priv->checked)
{
self->priv->checked = found;
atk_object_notify_state_change (ATK_OBJECT (self),
ATK_STATE_CHECKED,
found);
}
}
static void
on_can_focus_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data)
{
gboolean can_focus = st_widget_get_can_focus (ST_WIDGET (gobject));
atk_object_notify_state_change (ATK_OBJECT (data),
ATK_STATE_FOCUSABLE, can_focus);
}
static void
on_label_notify (GObject *gobject,
GParamSpec *pspec,
gpointer data)
{
check_labels (ST_WIDGET_ACCESSIBLE (data), ST_WIDGET (gobject));
}
static void
check_labels (StWidgetAccessible *widget_accessible,
StWidget *widget)
{
ClutterActor *label = NULL;
AtkObject *label_accessible = NULL;
/* We only call this method at startup, and when the label changes,
* so it is fine to remove the previous relationships if we have the
* current_label by default
*/
if (widget_accessible->priv->current_label != NULL)
{
AtkObject *previous_label = widget_accessible->priv->current_label;
atk_object_remove_relationship (ATK_OBJECT (widget_accessible),
ATK_RELATION_LABELLED_BY,
previous_label);
atk_object_remove_relationship (previous_label,
ATK_RELATION_LABEL_FOR,
ATK_OBJECT (widget_accessible));
g_object_unref (previous_label);
}
label = st_widget_get_label_actor (widget);
if (label == NULL)
{
widget_accessible->priv->current_label = NULL;
}
else
{
label_accessible = clutter_actor_get_accessible (label);
widget_accessible->priv->current_label = g_object_ref (label_accessible);
atk_object_add_relationship (ATK_OBJECT (widget_accessible),
ATK_RELATION_LABELLED_BY,
label_accessible);
atk_object_add_relationship (label_accessible,
ATK_RELATION_LABEL_FOR,
ATK_OBJECT (widget_accessible));
}
}
/**
* st_widget_get_focus_chain:
* @widget: An #StWidget
*
* Gets a list of the focusable children of @widget, in "Tab"
* order. By default, this returns all visible
* (as in CLUTTER_ACTOR_IS_VISIBLE()) children of @widget.
*
* Returns: (element-type Clutter.Actor) (transfer container):
* @widget's focusable children
*/
GList *
st_widget_get_focus_chain (StWidget *widget)
{
return ST_WIDGET_GET_CLASS (widget)->get_focus_chain (widget);
}