gnome-shell/src/st/st-widget.c
Jonas Dreßler 62e40a1350 st/widget: Invalidate paint volume on style changes
StWidget is overriding ClutterActors get_paint_volume() vfunc to adjust
for overpaint of things like box shadows that get set in CSS, it does
that by setting the paint volume to the theme nodes paint box.

Since StWidget can't really get notified when the paint box changes, we
just invalidate the paint volume when the theme nodes paint_equal()
returns FALSE.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1484>
2021-04-12 17:08:23 +00:00

3047 lines
88 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-settings.h"
#include "st-texture-cache.h"
#include "st-theme-context.h"
#include "st-theme-node-transition.h"
#include "st-theme-node-private.h"
#include "st-drawing-area.h"
#include "st-widget-accessible.h"
#include <atk/atk-enum-types.h>
/* This is set in stone and also hard-coded in GDK. */
#define VIRTUAL_CORE_POINTER_ID 2
/*
* Forward declaration for sake of StWidgetChild
*/
typedef struct _StWidgetPrivate StWidgetPrivate;
struct _StWidgetPrivate
{
StThemeNode *theme_node;
gchar *pseudo_class;
gchar *style_class;
gchar *inline_style;
StThemeNodeTransition *transition_animation;
guint is_style_dirty : 1;
guint first_child_dirty : 1;
guint last_child_dirty : 1;
guint draw_bg_color : 1;
guint draw_border_internal : 1;
guint track_hover : 1;
guint hover : 1;
guint can_focus : 1;
gulong texture_file_changed_id;
guint update_child_styles_id;
AtkObject *accessible;
AtkRole accessible_role;
AtkStateSet *local_state_set;
ClutterActor *label_actor;
gchar *accessible_name;
StWidget *last_visible_child;
StWidget *first_visible_child;
StThemeNodePaintState paint_states[2];
int current_paint_state : 2;
};
/**
* 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_PSEUDO_CLASS,
PROP_STYLE_CLASS,
PROP_STYLE,
PROP_TRACK_HOVER,
PROP_HOVER,
PROP_CAN_FOCUS,
PROP_LABEL_ACTOR,
PROP_ACCESSIBLE_ROLE,
PROP_ACCESSIBLE_NAME,
N_PROPS
};
static GParamSpec *props[N_PROPS] = { NULL, };
enum
{
STYLE_CHANGED,
POPUP_MENU,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL] = { 0, };
G_DEFINE_TYPE_WITH_PRIVATE (StWidget, st_widget, CLUTTER_TYPE_ACTOR);
#define ST_WIDGET_PRIVATE(w) ((StWidgetPrivate *)st_widget_get_instance_private (w))
static void st_widget_recompute_style (StWidget *widget,
StThemeNode *old_theme_node);
static gboolean st_widget_real_navigate_focus (StWidget *widget,
ClutterActor *from,
StDirectionType direction);
static AtkObject * st_widget_get_accessible (ClutterActor *actor);
static gboolean st_widget_has_accessible (ClutterActor *actor);
static void
st_widget_update_insensitive (StWidget *widget)
{
if (clutter_actor_get_reactive (CLUTTER_ACTOR (widget)))
st_widget_remove_style_pseudo_class (widget, "insensitive");
else
st_widget_add_style_pseudo_class (widget, "insensitive");
}
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_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_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;
case PROP_ACCESSIBLE_NAME:
st_widget_set_accessible_name (actor, g_value_get_string (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 = st_widget_get_instance_private (ST_WIDGET (gobject));
switch (prop_id)
{
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_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;
case PROP_ACCESSIBLE_NAME:
g_value_set_string (value, priv->accessible_name);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
break;
}
}
static void
st_widget_constructed (GObject *gobject)
{
G_OBJECT_CLASS (st_widget_parent_class)->constructed (gobject);
st_widget_update_insensitive (ST_WIDGET (gobject));
}
static void
st_widget_remove_transition (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->transition_animation)
{
g_object_run_dispose (G_OBJECT (priv->transition_animation));
g_object_unref (priv->transition_animation);
priv->transition_animation = NULL;
}
}
static void
next_paint_state (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
priv->current_paint_state = (priv->current_paint_state + 1) % G_N_ELEMENTS (priv->paint_states);
}
static StThemeNodePaintState *
current_paint_state (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
return &priv->paint_states[priv->current_paint_state];
}
static void
st_widget_texture_cache_changed (StTextureCache *cache,
GFile *file,
gpointer user_data)
{
StWidget *actor = ST_WIDGET (user_data);
StWidgetPrivate *priv = st_widget_get_instance_private (actor);
gboolean changed = FALSE;
int i;
for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++)
{
StThemeNodePaintState *paint_state = &priv->paint_states[i];
changed |= st_theme_node_paint_state_invalidate_for_file (paint_state, file);
}
if (changed && clutter_actor_is_mapped (CLUTTER_ACTOR (actor)))
clutter_actor_queue_redraw (CLUTTER_ACTOR (actor));
}
static void
st_widget_dispose (GObject *gobject)
{
StWidget *actor = ST_WIDGET (gobject);
StWidgetPrivate *priv = st_widget_get_instance_private (actor);
g_clear_pointer (&priv->theme_node, g_object_unref);
st_widget_remove_transition (actor);
g_clear_pointer (&priv->label_actor, g_object_unref);
g_clear_signal_handler (&priv->texture_file_changed_id,
st_texture_cache_get_default ());
g_clear_object (&priv->first_visible_child);
g_clear_object (&priv->last_visible_child);
G_OBJECT_CLASS (st_widget_parent_class)->dispose (gobject);
g_clear_handle_id (&priv->update_child_styles_id, g_source_remove);
}
static void
st_widget_finalize (GObject *gobject)
{
StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (gobject));
guint i;
g_free (priv->style_class);
g_free (priv->pseudo_class);
g_object_unref (priv->local_state_set);
g_free (priv->accessible_name);
g_free (priv->inline_style);
for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++)
st_theme_node_paint_state_free (&priv->paint_states[i]);
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)
{
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);
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);
}
/**
* st_widget_paint_background:
* @widget: The #StWidget
*
* Paint the background of the widget. This is meant to be called by
* subclasses of StWidget that need to paint the background without
* painting children.
*/
void
st_widget_paint_background (StWidget *widget,
ClutterPaintContext *paint_context)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
CoglFramebuffer *framebuffer;
StThemeNode *theme_node;
ClutterActorBox allocation;
float resource_scale;
guint8 opacity;
resource_scale = clutter_actor_get_resource_scale (CLUTTER_ACTOR (widget));
framebuffer = clutter_paint_context_get_framebuffer (paint_context);
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 (priv->transition_animation)
st_theme_node_transition_paint (priv->transition_animation,
framebuffer,
&allocation,
opacity,
resource_scale);
else
st_theme_node_paint (theme_node,
current_paint_state (widget),
framebuffer,
&allocation,
opacity,
resource_scale);
}
static void
st_widget_paint (ClutterActor *actor,
ClutterPaintContext *paint_context)
{
st_widget_paint_background (ST_WIDGET (actor), paint_context);
/* Chain up so we paint children. */
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->paint (actor, paint_context);
}
static void
st_widget_parent_set (ClutterActor *widget,
ClutterActor *old_parent)
{
StWidget *self = ST_WIDGET (widget);
ClutterActorClass *parent_class;
parent_class = CLUTTER_ACTOR_CLASS (st_widget_parent_class);
if (parent_class->parent_set)
parent_class->parent_set (widget, old_parent);
st_widget_style_changed (self);
}
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 = st_widget_get_instance_private (self);
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)
{
clutter_actor_queue_redraw ((ClutterActor *) self);
}
void
st_widget_style_changed (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
StThemeNode *old_theme_node = NULL;
priv->is_style_dirty = TRUE;
if (priv->theme_node)
{
old_theme_node = priv->theme_node;
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);
/* Descend through all children. If the actor is not mapped,
* children will clear their theme node without recomputing style.
*/
notify_children_of_style_change (CLUTTER_ACTOR (widget));
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.
*
* Returns: (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;
g_return_val_if_fail (ST_IS_WIDGET (widget), NULL);
priv = st_widget_get_instance_private (widget);
if (priv->theme_node == NULL)
{
StThemeContext *context;
StThemeNode *tmp_node;
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_autofree char *desc = st_describe_actor (CLUTTER_ACTOR (widget));
g_critical ("st_widget_get_theme_node called on the widget %s which is not in the stage.",
desc);
return g_object_new (ST_TYPE_THEME_NODE, NULL);
}
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 = (char *)"rtl";
else
direction_pseudo_class = (char *)"ltr";
if (priv->pseudo_class)
pseudo_class = g_strconcat(priv->pseudo_class, " ",
direction_pseudo_class, NULL);
else
pseudo_class = direction_pseudo_class;
context = st_theme_context_get_for_stage (stage);
tmp_node = st_theme_node_new (context, parent_node, NULL,
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);
priv->theme_node = g_object_ref (st_theme_context_intern_node (context,
tmp_node));
g_object_unref (tmp_node);
}
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.
*
* Returns: (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)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), NULL);
return ST_WIDGET_PRIVATE (widget)->theme_node;
}
static gboolean
st_widget_enter (ClutterActor *actor,
ClutterCrossingEvent *event)
{
StWidgetPrivate *priv = st_widget_get_instance_private (ST_WIDGET (actor));
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_get_instance_private (ST_WIDGET (actor));
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)))
{
st_widget_popup_menu (ST_WIDGET (actor));
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;
graphene_point3d_t 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_get_instance_private (ST_WIDGET (self));
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;
if (!clutter_actor_is_visible (child))
continue;
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, *l, *visible = NULL;
children = clutter_actor_get_children (CLUTTER_ACTOR (widget));
for (l = children; l; l = l->next)
{
if (clutter_actor_is_visible (CLUTTER_ACTOR (l->data)))
visible = g_list_prepend (visible, l->data);
}
g_list_free (children);
return g_list_reverse (visible);
}
static void
st_widget_resource_scale_changed (ClutterActor *actor)
{
StWidget *widget = ST_WIDGET (actor);
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
int i;
for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++)
st_theme_node_paint_state_invalidate (&priv->paint_states[i]);
if (CLUTTER_ACTOR_CLASS (st_widget_parent_class)->resource_scale_changed)
CLUTTER_ACTOR_CLASS (st_widget_parent_class)->resource_scale_changed (actor);
}
static void
st_widget_class_init (StWidgetClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
gobject_class->set_property = st_widget_set_property;
gobject_class->get_property = st_widget_get_property;
gobject_class->constructed = st_widget_constructed;
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;
actor_class->has_accessible = st_widget_has_accessible;
actor_class->resource_scale_changed = st_widget_resource_scale_changed;
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".
*/
props[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.
*/
props[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.
*/
props[PROP_STYLE] =
g_param_spec_string ("style",
"Style",
"Inline style string",
"",
ST_PARAM_READWRITE);
/**
* 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.
*/
props[PROP_TRACK_HOVER] =
g_param_spec_boolean ("track-hover",
"Track hover",
"Determines whether the widget tracks hover state",
FALSE,
ST_PARAM_READWRITE);
/**
* 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.
*/
props[PROP_HOVER] =
g_param_spec_boolean ("hover",
"Hover",
"Whether the pointer is hovering over the widget",
FALSE,
ST_PARAM_READWRITE);
/**
* StWidget:can-focus:
*
* Whether or not the widget can be focused via keyboard navigation.
*/
props[PROP_CAN_FOCUS] =
g_param_spec_boolean ("can-focus",
"Can focus",
"Whether the widget can be focused via keyboard navigation",
FALSE,
ST_PARAM_READWRITE);
/**
* StWidget:label-actor:
*
* An actor that labels this widget.
*/
props[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
*/
props[PROP_ACCESSIBLE_ROLE] =
g_param_spec_enum ("accessible-role",
"Accessible Role",
"The accessible role of this object",
ATK_TYPE_ROLE,
ATK_ROLE_INVALID,
ST_PARAM_READWRITE);
/**
* StWidget:accessible-name:
*
* Object instance's name for assistive technology access.
*/
props[PROP_ACCESSIBLE_NAME] =
g_param_spec_string ("accessible-name",
"Accessible name",
"Object instance's name for assistive technology access.",
NULL,
ST_PARAM_READWRITE);
g_object_class_install_properties (gobject_class, N_PROPS, props);
/**
* 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);
}
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: (nullable): 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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
priv = st_widget_get_instance_private (actor);
if (set_class_list (&priv->style_class, style_class_list))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (style_class != NULL);
priv = st_widget_get_instance_private (actor);
if (add_class_name (&priv->style_class, style_class))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (style_class != NULL);
priv = st_widget_get_instance_private (actor);
if (remove_class_name (&priv->style_class, style_class))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_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 ST_WIDGET_PRIVATE (actor)->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)
{
StWidgetPrivate *priv;
g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE);
priv = st_widget_get_instance_private (actor);
return find_class_name (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 ST_WIDGET_PRIVATE (actor)->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)
{
StWidgetPrivate *priv;
g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE);
priv = st_widget_get_instance_private (actor);
return find_class_name (priv->pseudo_class, pseudo_class) != NULL;
}
/**
* st_widget_set_style_pseudo_class:
* @actor: a #StWidget
* @pseudo_class_list: (nullable): 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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
priv = st_widget_get_instance_private (actor);
if (set_class_list (&priv->pseudo_class, pseudo_class_list))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (pseudo_class != NULL);
priv = st_widget_get_instance_private (actor);
if (add_class_name (&priv->pseudo_class, pseudo_class))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (actor));
g_return_if_fail (pseudo_class != NULL);
priv = st_widget_get_instance_private (actor);
if (remove_class_name (&priv->pseudo_class, pseudo_class))
{
st_widget_style_changed (actor);
g_object_notify_by_pspec (G_OBJECT (actor), props[PROP_PSEUDO_CLASS]);
}
}
/**
* st_widget_set_style:
* @actor: a #StWidget
* @style: (nullable): 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;
g_return_if_fail (ST_IS_WIDGET (actor));
priv = st_widget_get_instance_private (actor);
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_by_pspec (G_OBJECT (actor), props[PROP_STYLE]);
}
}
/**
* st_widget_get_style:
* @actor: a #StWidget
*
* Get the current inline style string. See st_widget_set_style().
*
* Returns: (transfer none) (nullable): 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 ST_WIDGET_PRIVATE (actor)->inline_style;
}
static void
st_widget_set_first_visible_child (StWidget *widget,
ClutterActor *actor)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->first_visible_child == NULL && actor == NULL)
return;
if (priv->first_visible_child != NULL &&
CLUTTER_ACTOR (priv->first_visible_child) == actor)
return;
if (priv->first_visible_child != NULL)
{
st_widget_remove_style_pseudo_class (priv->first_visible_child, "first-child");
g_clear_object (&priv->first_visible_child);
}
if (actor == NULL)
return;
if (ST_IS_WIDGET (actor))
{
st_widget_add_style_pseudo_class (ST_WIDGET (actor), "first-child");
priv->first_visible_child = g_object_ref (ST_WIDGET (actor));
}
}
static void
st_widget_set_last_visible_child (StWidget *widget,
ClutterActor *actor)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->last_visible_child == NULL && actor == NULL)
return;
if (priv->last_visible_child != NULL &&
CLUTTER_ACTOR (priv->last_visible_child) == actor)
return;
if (priv->last_visible_child != NULL)
{
st_widget_remove_style_pseudo_class (priv->last_visible_child, "last-child");
g_clear_object (&priv->last_visible_child);
}
if (actor == NULL)
return;
if (ST_IS_WIDGET (actor))
{
st_widget_add_style_pseudo_class (ST_WIDGET (actor), "last-child");
priv->last_visible_child = g_object_ref (ST_WIDGET (actor));
}
}
static void
st_widget_name_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
st_widget_style_changed (widget);
}
static void
st_widget_reactive_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
st_widget_update_insensitive (widget);
if (priv->track_hover)
st_widget_sync_hover(widget);
}
static ClutterActor *
find_nearest_visible_backwards (ClutterActor *actor)
{
ClutterActor *prev = actor;
while (prev != NULL && !clutter_actor_is_visible (prev))
prev = clutter_actor_get_previous_sibling (prev);
return prev;
}
static ClutterActor *
find_nearest_visible_forward (ClutterActor *actor)
{
ClutterActor *next = actor;
while (next != NULL && !clutter_actor_is_visible (next))
next = clutter_actor_get_next_sibling (next);
return next;
}
static gboolean
st_widget_update_child_styles (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->first_child_dirty)
{
ClutterActor *first_child;
priv->first_child_dirty = FALSE;
first_child = clutter_actor_get_first_child (CLUTTER_ACTOR (widget));
st_widget_set_first_visible_child (widget,
find_nearest_visible_forward (first_child));
}
if (priv->last_child_dirty)
{
ClutterActor *last_child;
priv->last_child_dirty = FALSE;
last_child = clutter_actor_get_last_child (CLUTTER_ACTOR (widget));
st_widget_set_last_visible_child (widget,
find_nearest_visible_backwards (last_child));
}
priv->update_child_styles_id = 0;
return G_SOURCE_REMOVE;
}
static void
st_widget_queue_child_styles_update (StWidget *widget)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->update_child_styles_id != 0)
return;
priv->update_child_styles_id = g_idle_add ((GSourceFunc) st_widget_update_child_styles, widget);
}
static void
st_widget_visible_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
StWidgetPrivate *parent_priv;
ClutterActor *actor = CLUTTER_ACTOR (widget);
ClutterActor *parent = clutter_actor_get_parent (actor);
if (parent == NULL || !ST_IS_WIDGET (parent))
return;
parent_priv = st_widget_get_instance_private (ST_WIDGET (parent));
if (clutter_actor_is_visible (actor))
{
ClutterActor *before, *after;
before = clutter_actor_get_previous_sibling (actor);
if (find_nearest_visible_backwards (before) == NULL)
parent_priv->first_child_dirty = TRUE;
after = clutter_actor_get_next_sibling (actor);
if (find_nearest_visible_forward (after) == NULL)
parent_priv->last_child_dirty = TRUE;
}
else
{
if (st_widget_has_style_pseudo_class (widget, "first-child"))
parent_priv->first_child_dirty = TRUE;
if (st_widget_has_style_pseudo_class (widget, "last-child"))
parent_priv->last_child_dirty = TRUE;
}
if (parent_priv->first_child_dirty || parent_priv->last_child_dirty)
st_widget_queue_child_styles_update (ST_WIDGET (parent));
}
static void
st_widget_first_child_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
priv->first_child_dirty = TRUE;
st_widget_queue_child_styles_update (widget);
}
static void
st_widget_last_child_notify (StWidget *widget,
GParamSpec *pspec,
gpointer data)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
priv->last_child_dirty = TRUE;
st_widget_queue_child_styles_update (widget);
}
static void
st_widget_init (StWidget *actor)
{
StWidgetPrivate *priv;
guint i;
priv = st_widget_get_instance_private (actor);
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::reactive", G_CALLBACK (st_widget_reactive_notify), NULL);
g_signal_connect (actor, "notify::visible", G_CALLBACK (st_widget_visible_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);
priv->texture_file_changed_id = g_signal_connect (st_texture_cache_get_default (), "texture-file-changed",
G_CALLBACK (st_widget_texture_cache_changed), actor);
for (i = 0; i < G_N_ELEMENTS (priv->paint_states); i++)
st_theme_node_paint_state_init (&priv->paint_states[i]);
}
static void
on_transition_completed (StThemeNodeTransition *transition,
StWidget *widget)
{
next_paint_state (widget);
st_theme_node_paint_state_copy (current_paint_state (widget),
st_theme_node_transition_get_new_paint_state (transition));
st_widget_remove_transition (widget);
}
static void
st_widget_recompute_style (StWidget *widget,
StThemeNode *old_theme_node)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
StThemeNode *new_theme_node = st_widget_get_theme_node (widget);
int transition_duration;
StSettings *settings;
gboolean paint_equal, geometry_equal = FALSE;
gboolean animations_enabled;
if (new_theme_node == old_theme_node)
{
priv->is_style_dirty = FALSE;
return;
}
_st_theme_node_apply_margins (new_theme_node, CLUTTER_ACTOR (widget));
if (old_theme_node)
geometry_equal = st_theme_node_geometry_equal (old_theme_node, new_theme_node);
if (!geometry_equal)
clutter_actor_queue_relayout ((ClutterActor *) widget);
transition_duration = st_theme_node_get_transition_duration (new_theme_node);
paint_equal = st_theme_node_paint_equal (old_theme_node, new_theme_node);
settings = st_settings_get ();
g_object_get (settings, "enable-animations", &animations_enabled, NULL);
if (animations_enabled && transition_duration > 0)
{
if (priv->transition_animation != NULL)
{
st_theme_node_transition_update (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.
*/
priv->transition_animation =
st_theme_node_transition_new (CLUTTER_ACTOR (widget),
old_theme_node,
new_theme_node,
current_paint_state (widget),
transition_duration);
g_signal_connect (priv->transition_animation, "completed",
G_CALLBACK (on_transition_completed), widget);
g_signal_connect_swapped (priv->transition_animation,
"new-frame",
G_CALLBACK (clutter_actor_queue_redraw),
widget);
}
}
else if (priv->transition_animation)
{
st_widget_remove_transition (widget);
}
if (!paint_equal)
{
clutter_actor_invalidate_paint_volume (CLUTTER_ACTOR (widget));
next_paint_state (widget);
if (!st_theme_node_paint_equal (new_theme_node, current_paint_state (widget)->node))
st_theme_node_paint_state_invalidate (current_paint_state (widget));
}
/* It is very likely that custom CSS properties are used with StDrawingArea
to control the custom drawing, so never omit the ::style-changed signal */
if (paint_equal)
paint_equal = !ST_IS_DRAWING_AREA (widget);
if (paint_equal && old_theme_node->font_desc != NULL)
paint_equal = pango_font_description_equal (old_theme_node->font_desc,
st_theme_node_get_font (new_theme_node));
if (paint_equal && old_theme_node->foreground_computed)
{
ClutterColor col;
st_theme_node_get_foreground_color (new_theme_node, &col);
paint_equal = clutter_color_equal (&old_theme_node->foreground_color, &col);
}
if (paint_equal && old_theme_node->icon_colors)
paint_equal = st_icon_colors_equal (old_theme_node->icon_colors,
st_theme_node_get_icon_colors (new_theme_node));
if (!paint_equal || !geometry_equal)
g_signal_emit (widget, signals[STYLE_CHANGED], 0);
priv->is_style_dirty = FALSE;
}
/**
* st_widget_ensure_style:
* @widget: A #StWidget
*
* Ensures that @widget has read its style information and propagated any
* changes to its children.
*/
void
st_widget_ensure_style (StWidget *widget)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
if (priv->is_style_dirty)
{
st_widget_recompute_style (widget, NULL);
notify_children_of_style_change (CLUTTER_ACTOR (widget));
}
}
/**
* 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 = st_widget_get_instance_private (widget);
if (priv->track_hover != track_hover)
{
priv->track_hover = track_hover;
g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_TRACK_HOVER]);
if (priv->track_hover)
st_widget_sync_hover (widget);
else
st_widget_set_hover (widget, FALSE);
}
}
/**
* st_widget_get_track_hover:
* @widget: A #StWidget
*
* Returns the current value of the #StWidget: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 ST_WIDGET_PRIVATE (widget)->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 = st_widget_get_instance_private (widget);
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_by_pspec (G_OBJECT (widget), props[PROP_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)
{
ClutterInputDevice *pointer;
ClutterActor *stage;
ClutterActor *pointer_actor;
ClutterSeat *seat;
seat = clutter_backend_get_default_seat (clutter_get_default_backend ());
pointer = clutter_seat_get_pointer (seat);
stage = clutter_actor_get_stage (CLUTTER_ACTOR (widget));
if (!stage)
return;
pointer_actor = clutter_stage_get_device_actor (CLUTTER_STAGE (stage), pointer, NULL);
if (pointer_actor && clutter_actor_get_reactive (CLUTTER_ACTOR (widget)))
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 ST_WIDGET_PRIVATE (widget)->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 = st_widget_get_instance_private (widget);
if (priv->can_focus != can_focus)
{
priv->can_focus = can_focus;
g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_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 ST_WIDGET_PRIVATE (widget)->can_focus;
}
/**
* st_widget_popup_menu:
* @self: A #StWidget
*
* Asks the widget to pop-up a context menu by emitting #StWidget::popup-menu.
*/
void
st_widget_popup_menu (StWidget *self)
{
g_signal_emit (self, signals[POPUP_MENU], 0);
}
/* 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,
StDirectionType direction)
{
ClutterActorBox cbox;
graphene_point3d_t 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 ST_DIR_UP:
if (cbox.y2 > rbox->y1 + 0.1)
continue;
break;
case ST_DIR_DOWN:
if (cbox.y1 < rbox->y2 - 0.1)
continue;
break;
case ST_DIR_LEFT:
if (cbox.x2 > rbox->x1 + 0.1)
continue;
break;
case ST_DIR_RIGHT:
if (cbox.x1 < rbox->x2 - 0.1)
continue;
break;
case ST_DIR_TAB_BACKWARD:
case ST_DIR_TAB_FORWARD:
default:
g_return_val_if_reached (NULL);
}
ret = g_list_prepend (ret, child);
}
g_list_free (children);
return ret;
}
static void
get_midpoint (ClutterActorBox *box,
int *x,
int *y)
{
*x = (box->x1 + box->x2) / 2;
*y = (box->y1 + box->y2) / 2;
}
static double
get_distance (ClutterActor *actor,
ClutterActorBox *bbox)
{
int ax, ay, bx, by, dx, dy;
ClutterActorBox abox;
graphene_point3d_t abs_vertices[4];
clutter_actor_get_abs_allocation_vertices (actor, abs_vertices);
clutter_actor_box_from_vertices (&abox, abs_vertices);
get_midpoint (&abox, &ax, &ay);
get_midpoint (bbox, &bx, &by);
dx = ax - bx;
dy = ay - by;
/* Not the exact distance, but good enough to sort by. */
return dx*dx + dy*dy;
}
static int
sort_by_distance (gconstpointer a,
gconstpointer b,
gpointer user_data)
{
ClutterActor *actor_a = (ClutterActor *)a;
ClutterActor *actor_b = (ClutterActor *)b;
ClutterActorBox *box = user_data;
return get_distance (actor_a, box) - get_distance (actor_b, box);
}
static gboolean
st_widget_real_navigate_focus (StWidget *widget,
ClutterActor *from,
StDirectionType direction)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
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 (priv->can_focus)
{
if (!focus_child)
{
if (clutter_actor_is_mapped (widget_actor))
{
/* Accept focus from outside */
clutter_actor_grab_key_focus (widget_actor);
return TRUE;
}
else
{
/* Refuse to set focus on hidden actors */
return FALSE;
}
}
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 == ST_DIR_TAB_FORWARD ||
direction == ST_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 == ST_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 */
{
ClutterActorBox sort_box;
graphene_point3d_t 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_box, abs_vertices);
}
else
{
clutter_actor_get_abs_allocation_vertices (widget_actor, abs_vertices);
clutter_actor_box_from_vertices (&sort_box, abs_vertices);
switch (direction)
{
case ST_DIR_UP:
sort_box.y1 = sort_box.y2;
break;
case ST_DIR_DOWN:
sort_box.y2 = sort_box.y1;
break;
case ST_DIR_LEFT:
sort_box.x1 = sort_box.x2;
break;
case ST_DIR_RIGHT:
sort_box.x2 = sort_box.x1;
break;
case ST_DIR_TAB_FORWARD:
case ST_DIR_TAB_BACKWARD:
default:
g_warn_if_reached ();
}
}
if (from)
children = filter_by_position (children, &sort_box, direction);
if (children)
children = g_list_sort_with_data (children, sort_by_distance, &sort_box);
}
/* 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: (nullable): 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.
*
* Returns: %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,
StDirectionType 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.
*
* Returns: 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_widget_get_label_actor:
* @widget: a #StWidget
*
* Gets the label that identifies @widget if it is defined
*
* Returns: (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 ST_WIDGET_PRIVATE (widget)->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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
if (priv->label_actor != label)
{
if (priv->label_actor)
g_object_unref (priv->label_actor);
if (label != NULL)
priv->label_actor = g_object_ref (label);
else
priv->label_actor = NULL;
g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_LABEL_ACTOR]);
}
}
/**
* st_widget_set_accessible_name:
* @widget: widget to set the accessible name for
* @name: (nullable): a character string to be set as the accessible name
*
* This method sets @name as the accessible name for @widget.
*
* Usually you will have no need to set the accessible name for an
* object, as usually there is a label for most of the interface
* elements. So in general it is better to just use
* @st_widget_set_label_actor. This method is only required when you
* need to set an accessible name and there is no available label
* object.
*
*/
void
st_widget_set_accessible_name (StWidget *widget,
const gchar *name)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
if (priv->accessible_name != NULL)
g_free (priv->accessible_name);
priv->accessible_name = g_strdup (name);
g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_ACCESSIBLE_NAME]);
}
/**
* st_widget_get_accessible_name:
* @widget: widget to get the accessible name for
*
* Gets the accessible name for this widget. See
* st_widget_set_accessible_name() for more information.
*
* Returns: a character string representing the accessible name
* of the widget.
*/
const gchar *
st_widget_get_accessible_name (StWidget *widget)
{
g_return_val_if_fail (ST_IS_WIDGET (widget), NULL);
return ST_WIDGET_PRIVATE (widget)->accessible_name;
}
/**
* 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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
priv->accessible_role = role;
g_object_notify_by_pspec (G_OBJECT (widget), props[PROP_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.
*
* Returns: accessible #AtkRole for this widget
*/
AtkRole
st_widget_get_accessible_role (StWidget *widget)
{
StWidgetPrivate *priv;
AtkRole role = ATK_ROLE_INVALID;
g_return_val_if_fail (ST_IS_WIDGET (widget), ATK_ROLE_INVALID);
priv = st_widget_get_instance_private (widget);
if (priv->accessible_role != ATK_ROLE_INVALID)
role = priv->accessible_role;
else if (priv->accessible != NULL)
role = atk_object_get_role (priv->accessible);
return role;
}
static void
notify_accessible_state_change (StWidget *widget,
AtkStateType state,
gboolean value)
{
StWidgetPrivate *priv = st_widget_get_instance_private (widget);
if (priv->accessible != NULL)
atk_object_notify_state_change (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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
if (atk_state_set_add_state (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)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
priv = st_widget_get_instance_private (widget);
if (atk_state_set_remove_state (priv->local_state_set, state))
notify_accessible_state_change (widget, state, FALSE);
}
/******************************************************************************/
/*************************** ACCESSIBILITY SUPPORT ****************************/
/******************************************************************************/
/* GObject */
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);
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;
};
G_DEFINE_TYPE_WITH_PRIVATE (StWidgetAccessible, st_widget_accessible, CALLY_TYPE_ACTOR)
static gboolean
st_widget_has_accessible (ClutterActor *actor)
{
StWidget *widget;
StWidgetPrivate *priv;
g_return_val_if_fail (ST_IS_WIDGET (actor), FALSE);
widget = ST_WIDGET (actor);
priv = st_widget_get_instance_private (widget);
return priv->accessible != NULL;
}
static AtkObject *
st_widget_get_accessible (ClutterActor *actor)
{
StWidget *widget = NULL;
StWidgetPrivate *priv;
g_return_val_if_fail (ST_IS_WIDGET (actor), NULL);
widget = ST_WIDGET (actor);
priv = st_widget_get_instance_private (widget);
if (priv->accessible == NULL)
{
priv->accessible =
g_object_new (ST_WIDGET_GET_CLASS (widget)->get_accessible_type (),
NULL);
atk_object_initialize (priv->accessible, actor);
/* AtkGObjectAccessible, which StWidgetAccessible derives from, clears
* the back reference to the object in a weak notify for the object;
* weak-ref notification, which occurs during g_object_real_dispose(),
* is then the optimal time to clear the forward reference. We
* can't clear the reference in dispose() before chaining up, since
* clutter_actor_dispose() causes notifications to be sent out, which
* will result in a new accessible object being created.
*/
g_object_add_weak_pointer (G_OBJECT (actor),
(gpointer *)&priv->accessible);
}
return priv->accessible;
}
/**
* st_widget_set_accessible:
* @widget: A #StWidget
* @accessible: an accessible (#AtkObject)
*
* This method allows to set a customly created accessible object to
* this widget. For example if you define a new subclass of
* #StWidgetAccessible at the javascript code.
*
* NULL is a valid value for @accessible. That contemplates the
* hypothetical case of not needing anymore a custom accessible object
* for the widget. Next call of st_widget_get_accessible() would
* create and return a default accessible.
*
* It assumes that the call to atk_object_initialize that bound the
* gobject with the custom accessible object was already called, so
* not a responsibility of this method.
*
*/
void
st_widget_set_accessible (StWidget *widget,
AtkObject *accessible)
{
StWidgetPrivate *priv;
g_return_if_fail (ST_IS_WIDGET (widget));
g_return_if_fail (accessible == NULL || ATK_IS_GOBJECT_ACCESSIBLE (accessible));
priv = st_widget_get_instance_private (widget);
if (priv->accessible != accessible)
{
if (priv->accessible)
{
g_object_remove_weak_pointer (G_OBJECT (widget),
(gpointer *)&priv->accessible);
g_object_unref (priv->accessible);
priv->accessible = NULL;
}
if (accessible)
{
priv->accessible = g_object_ref (accessible);
/* See note in st_widget_get_accessible() */
g_object_add_weak_pointer (G_OBJECT (widget),
(gpointer *)&priv->accessible);
}
else
priv->accessible = NULL;
}
}
static const gchar *
st_widget_accessible_get_name (AtkObject *obj)
{
const gchar* name = NULL;
g_return_val_if_fail (ST_IS_WIDGET_ACCESSIBLE (obj), NULL);
name = ATK_OBJECT_CLASS (st_widget_accessible_parent_class)->get_name (obj);
if (name == NULL)
{
StWidget *widget = NULL;
widget = ST_WIDGET (atk_gobject_accessible_get_object (ATK_GOBJECT_ACCESSIBLE (obj)));
if (widget == NULL)
name = NULL;
else
name = st_widget_get_accessible_name (widget);
}
return name;
}
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;
atk_class->get_name = st_widget_accessible_get_name;
}
static void
st_widget_accessible_init (StWidgetAccessible *self)
{
StWidgetAccessiblePrivate *priv = st_widget_accessible_get_instance_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;
}
G_OBJECT_CLASS (st_widget_accessible_parent_class)->dispose (gobject);
}
static void
on_accessible_name_notify (GObject *gobject,
GParamSpec *pspec,
AtkObject *accessible)
{
g_object_notify (G_OBJECT (accessible), "accessible-name");
}
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);
g_signal_connect (data, "notify::accessible-name",
G_CALLBACK (on_accessible_name_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;
StWidgetPrivate *widget_priv;
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);
widget_priv = st_widget_get_instance_private (widget);
/* 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;
StWidgetPrivate *priv;
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;
priv = st_widget_get_instance_private (widget);
if (priv->accessible_role != ATK_ROLE_INVALID)
return 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);
}