1cefd589da
With commitsfab39bbea5
and62e40a1350
we started depending on a new ClutterActor API: clutter_actor_invalidate_paint_volume() Given that this commit was applied to the 40 stable release, it broke ABI compatibility with mutter, which is something we guarantee between stable releases. So use GModule to dynamically find the symbol in our loaded libraries. If it exists, use it and invalidate the paint volume. If it doesn't exist, libmutter is still at version 40.0 and we don't need to invalidate the paint volume. This also adds a dependency on gmodule. We need to link against gmodule to use g_module_open() and g_module_symbol() APIs. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1807>
3059 lines
88 KiB
C
3059 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)
|
|
{
|
|
static gboolean invalidate_paint_volume_valid = FALSE;
|
|
static void (* invalidate_paint_volume) (ClutterActor *) = NULL;
|
|
|
|
if (!invalidate_paint_volume_valid)
|
|
{
|
|
g_module_symbol (g_module_open (NULL, G_MODULE_BIND_LAZY),
|
|
"clutter_actor_invalidate_paint_volume",
|
|
(gpointer *)&invalidate_paint_volume);
|
|
invalidate_paint_volume_valid = TRUE;
|
|
}
|
|
|
|
if (invalidate_paint_volume)
|
|
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);
|
|
}
|