1118ec9653
The signals for the action group were being disconnected when the action group was explicitly removed from the GActionMuxer but the same was not being done when it was finalized. This means that a change in the state of an action group that used to be associated with a finalized GActionMuxer would result in a crash. This would happen for stateful application actions after closing a window. https://bugzilla.gnome.org/show_bug.cgi?id=681399
547 lines
17 KiB
C
547 lines
17 KiB
C
/*
|
|
* Copyright © 2011 Canonical Limited
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the licence, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*
|
|
* Author: Ryan Lortie <desrt@desrt.ca>
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "gactionmuxer.h"
|
|
|
|
#include "gactionobservable.h"
|
|
#include "gactionobserver.h"
|
|
|
|
#include <clutter/clutter.h>
|
|
|
|
#include <string.h>
|
|
|
|
/*
|
|
* SECTION:gactionmuxer
|
|
* @short_description: Aggregate and monitor several action groups
|
|
*
|
|
* #GActionMuxer is a #GActionGroup and #GActionObservable that is
|
|
* capable of containing other #GActionGroup instances.
|
|
*
|
|
* The typical use is aggregating all of the actions applicable to a
|
|
* particular context into a single action group, with namespacing.
|
|
*
|
|
* Consider the case of two action groups -- one containing actions
|
|
* applicable to an entire application (such as 'quit') and one
|
|
* containing actions applicable to a particular window in the
|
|
* application (such as 'fullscreen').
|
|
*
|
|
* In this case, each of these action groups could be added to a
|
|
* #GActionMuxer with the prefixes "app" and "win", respectively. This
|
|
* would expose the actions as "app.quit" and "win.fullscreen" on the
|
|
* #GActionGroup interface presented by the #GActionMuxer.
|
|
*
|
|
* Activations and state change requests on the #GActionMuxer are wired
|
|
* through to the underlying action group in the expected way.
|
|
*
|
|
* This class is typically only used at the site of "consumption" of
|
|
* actions (eg: when displaying a menu that contains many actions on
|
|
* different objects).
|
|
*/
|
|
|
|
static void g_action_muxer_group_iface_init (GActionGroupInterface *iface);
|
|
static void g_action_muxer_observable_iface_init (GActionObservableInterface *iface);
|
|
|
|
typedef GObjectClass GActionMuxerClass;
|
|
|
|
struct _GActionMuxer
|
|
{
|
|
GObject parent_instance;
|
|
|
|
GHashTable *actions;
|
|
GHashTable *groups;
|
|
};
|
|
|
|
G_DEFINE_TYPE_WITH_CODE (GActionMuxer, g_action_muxer, G_TYPE_OBJECT,
|
|
G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, g_action_muxer_group_iface_init)
|
|
G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_OBSERVABLE, g_action_muxer_observable_iface_init))
|
|
|
|
typedef struct
|
|
{
|
|
GActionMuxer *muxer;
|
|
GSList *watchers;
|
|
gchar *fullname;
|
|
} Action;
|
|
|
|
typedef struct
|
|
{
|
|
GActionMuxer *muxer;
|
|
GActionGroup *group;
|
|
gchar *prefix;
|
|
gulong handler_ids[4];
|
|
} Group;
|
|
|
|
static gchar **
|
|
g_action_muxer_list_actions (GActionGroup *action_group)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (action_group);
|
|
GHashTableIter iter;
|
|
gchar *key;
|
|
gchar **keys;
|
|
gsize i;
|
|
|
|
keys = g_new (gchar *, g_hash_table_size (muxer->actions) + 1);
|
|
|
|
i = 0;
|
|
g_hash_table_iter_init (&iter, muxer->actions);
|
|
while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL))
|
|
keys[i++] = g_strdup (key);
|
|
keys[i] = NULL;
|
|
|
|
return keys;
|
|
}
|
|
|
|
static Group *
|
|
g_action_muxer_find_group (GActionMuxer *muxer,
|
|
const gchar **name)
|
|
{
|
|
const gchar *dot;
|
|
gchar *prefix;
|
|
Group *group;
|
|
|
|
dot = strchr (*name, '.');
|
|
|
|
if (!dot)
|
|
return NULL;
|
|
|
|
prefix = g_strndup (*name, dot - *name);
|
|
group = g_hash_table_lookup (muxer->groups, prefix);
|
|
g_free (prefix);
|
|
|
|
*name = dot + 1;
|
|
|
|
return group;
|
|
}
|
|
|
|
static Action *
|
|
g_action_muxer_lookup_action (GActionMuxer *muxer,
|
|
const gchar *prefix,
|
|
const gchar *action_name,
|
|
gchar **fullname)
|
|
{
|
|
Action *action;
|
|
|
|
*fullname = g_strconcat (prefix, ".", action_name, NULL);
|
|
action = g_hash_table_lookup (muxer->actions, *fullname);
|
|
|
|
return action;
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_action_enabled_changed (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
gboolean enabled,
|
|
gpointer user_data)
|
|
{
|
|
Group *group = user_data;
|
|
gchar *fullname;
|
|
Action *action;
|
|
GSList *node;
|
|
|
|
action = g_action_muxer_lookup_action (group->muxer, group->prefix, action_name, &fullname);
|
|
for (node = action ? action->watchers : NULL; node; node = node->next)
|
|
g_action_observer_action_enabled_changed (node->data, G_ACTION_OBSERVABLE (group->muxer), fullname, enabled);
|
|
g_action_group_action_enabled_changed (G_ACTION_GROUP (group->muxer), fullname, enabled);
|
|
g_free (fullname);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_action_state_changed (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
GVariant *state,
|
|
gpointer user_data)
|
|
{
|
|
Group *group = user_data;
|
|
gchar *fullname;
|
|
Action *action;
|
|
GSList *node;
|
|
|
|
action = g_action_muxer_lookup_action (group->muxer, group->prefix, action_name, &fullname);
|
|
for (node = action ? action->watchers : NULL; node; node = node->next)
|
|
g_action_observer_action_state_changed (node->data, G_ACTION_OBSERVABLE (group->muxer), fullname, state);
|
|
g_action_group_action_state_changed (G_ACTION_GROUP (group->muxer), fullname, state);
|
|
g_free (fullname);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_action_added (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
gpointer user_data)
|
|
{
|
|
const GVariantType *parameter_type;
|
|
Group *group = user_data;
|
|
gboolean enabled;
|
|
GVariant *state;
|
|
|
|
if (g_action_group_query_action (group->group, action_name, &enabled, ¶meter_type, NULL, NULL, &state))
|
|
{
|
|
gchar *fullname;
|
|
Action *action;
|
|
GSList *node;
|
|
|
|
action = g_action_muxer_lookup_action (group->muxer, group->prefix, action_name, &fullname);
|
|
|
|
for (node = action ? action->watchers : NULL; node; node = node->next)
|
|
g_action_observer_action_added (node->data,
|
|
G_ACTION_OBSERVABLE (group->muxer),
|
|
fullname, parameter_type, enabled, state);
|
|
|
|
g_action_group_action_added (G_ACTION_GROUP (group->muxer), fullname);
|
|
|
|
if (state)
|
|
g_variant_unref (state);
|
|
|
|
g_free (fullname);
|
|
}
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_action_removed (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
gpointer user_data)
|
|
{
|
|
Group *group = user_data;
|
|
gchar *fullname;
|
|
Action *action;
|
|
GSList *node;
|
|
|
|
action = g_action_muxer_lookup_action (group->muxer, group->prefix, action_name, &fullname);
|
|
for (node = action ? action->watchers : NULL; node; node = node->next)
|
|
g_action_observer_action_removed (node->data, G_ACTION_OBSERVABLE (group->muxer), fullname);
|
|
g_action_group_action_removed (G_ACTION_GROUP (group->muxer), fullname);
|
|
g_free (fullname);
|
|
}
|
|
|
|
static gboolean
|
|
g_action_muxer_query_action (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
gboolean *enabled,
|
|
const GVariantType **parameter_type,
|
|
const GVariantType **state_type,
|
|
GVariant **state_hint,
|
|
GVariant **state)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (action_group);
|
|
Group *group;
|
|
|
|
group = g_action_muxer_find_group (muxer, &action_name);
|
|
|
|
if (!group)
|
|
return FALSE;
|
|
|
|
return g_action_group_query_action (group->group, action_name, enabled,
|
|
parameter_type, state_type, state_hint, state);
|
|
}
|
|
|
|
static GVariant *
|
|
get_platform_data (void)
|
|
{
|
|
gchar time[32];
|
|
GVariantBuilder *builder;
|
|
GVariant *result;
|
|
|
|
g_snprintf (time, 32, "_TIME%d", clutter_get_current_event_time ());
|
|
|
|
builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
|
|
|
|
g_variant_builder_add (builder, "{sv}", "desktop-startup-id",
|
|
g_variant_new_string (time));
|
|
|
|
result = g_variant_builder_end (builder);
|
|
g_variant_builder_unref (builder);
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_activate_action (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
GVariant *parameter)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (action_group);
|
|
Group *group;
|
|
|
|
group = g_action_muxer_find_group (muxer, &action_name);
|
|
|
|
if (group)
|
|
{
|
|
if (G_IS_REMOTE_ACTION_GROUP (group->group))
|
|
g_remote_action_group_activate_action_full (G_REMOTE_ACTION_GROUP (group->group),
|
|
action_name,
|
|
parameter,
|
|
get_platform_data ());
|
|
else
|
|
g_action_group_activate_action (group->group, action_name, parameter);
|
|
}
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_change_action_state (GActionGroup *action_group,
|
|
const gchar *action_name,
|
|
GVariant *state)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (action_group);
|
|
Group *group;
|
|
|
|
group = g_action_muxer_find_group (muxer, &action_name);
|
|
|
|
if (group)
|
|
{
|
|
if (G_IS_REMOTE_ACTION_GROUP (group->group))
|
|
g_remote_action_group_change_action_state_full (G_REMOTE_ACTION_GROUP (group->group),
|
|
action_name,
|
|
state,
|
|
get_platform_data ());
|
|
else
|
|
g_action_group_change_action_state (group->group, action_name, state);
|
|
}
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_unregister_internal (Action *action,
|
|
gpointer observer)
|
|
{
|
|
GActionMuxer *muxer = action->muxer;
|
|
GSList **ptr;
|
|
|
|
for (ptr = &action->watchers; *ptr; ptr = &(*ptr)->next)
|
|
if ((*ptr)->data == observer)
|
|
{
|
|
*ptr = g_slist_remove (*ptr, observer);
|
|
|
|
if (action->watchers == NULL)
|
|
{
|
|
g_hash_table_remove (muxer->actions, action->fullname);
|
|
g_free (action->fullname);
|
|
|
|
g_slice_free (Action, action);
|
|
|
|
g_object_unref (muxer);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_weak_notify (gpointer data,
|
|
GObject *where_the_object_was)
|
|
{
|
|
Action *action = data;
|
|
|
|
g_action_muxer_unregister_internal (action, where_the_object_was);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_register_observer (GActionObservable *observable,
|
|
const gchar *name,
|
|
GActionObserver *observer)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (observable);
|
|
Action *action;
|
|
|
|
action = g_hash_table_lookup (muxer->actions, name);
|
|
|
|
if (action == NULL)
|
|
{
|
|
action = g_slice_new (Action);
|
|
action->muxer = g_object_ref (muxer);
|
|
action->fullname = g_strdup (name);
|
|
action->watchers = NULL;
|
|
|
|
g_hash_table_insert (muxer->actions, action->fullname, action);
|
|
}
|
|
|
|
action->watchers = g_slist_prepend (action->watchers, observer);
|
|
g_object_weak_ref (G_OBJECT (observer), g_action_muxer_weak_notify, action);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_unregister_observer (GActionObservable *observable,
|
|
const gchar *name,
|
|
GActionObserver *observer)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (observable);
|
|
Action *action;
|
|
|
|
action = g_hash_table_lookup (muxer->actions, name);
|
|
g_object_weak_unref (G_OBJECT (observer), g_action_muxer_weak_notify, action);
|
|
g_action_muxer_unregister_internal (action, observer);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_free_group (gpointer data)
|
|
{
|
|
Group *group = data;
|
|
gint i;
|
|
|
|
/* 'for loop' or 'four loop'? */
|
|
for (i = 0; i < 4; i++)
|
|
g_signal_handler_disconnect (group->group, group->handler_ids[i]);
|
|
|
|
g_object_unref (group->group);
|
|
g_free (group->prefix);
|
|
|
|
g_slice_free (Group, group);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_finalize (GObject *object)
|
|
{
|
|
GActionMuxer *muxer = G_ACTION_MUXER (object);
|
|
|
|
g_assert_cmpint (g_hash_table_size (muxer->actions), ==, 0);
|
|
g_hash_table_unref (muxer->actions);
|
|
g_hash_table_unref (muxer->groups);
|
|
|
|
G_OBJECT_CLASS (g_action_muxer_parent_class)
|
|
->finalize (object);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_init (GActionMuxer *muxer)
|
|
{
|
|
muxer->actions = g_hash_table_new (g_str_hash, g_str_equal);
|
|
muxer->groups = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_action_muxer_free_group);
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_observable_iface_init (GActionObservableInterface *iface)
|
|
{
|
|
iface->register_observer = g_action_muxer_register_observer;
|
|
iface->unregister_observer = g_action_muxer_unregister_observer;
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_group_iface_init (GActionGroupInterface *iface)
|
|
{
|
|
iface->list_actions = g_action_muxer_list_actions;
|
|
iface->query_action = g_action_muxer_query_action;
|
|
iface->activate_action = g_action_muxer_activate_action;
|
|
iface->change_action_state = g_action_muxer_change_action_state;
|
|
}
|
|
|
|
static void
|
|
g_action_muxer_class_init (GObjectClass *class)
|
|
{
|
|
class->finalize = g_action_muxer_finalize;
|
|
}
|
|
|
|
/*
|
|
* g_action_muxer_insert:
|
|
* @muxer: a #GActionMuxer
|
|
* @prefix: the prefix string for the action group
|
|
* @action_group: a #GActionGroup
|
|
*
|
|
* Adds the actions in @action_group to the list of actions provided by
|
|
* @muxer. @prefix is prefixed to each action name, such that for each
|
|
* action <varname>x</varname> in @action_group, there is an equivalent
|
|
* action @prefix<literal>.</literal><varname>x</varname> in @muxer.
|
|
*
|
|
* For example, if @prefix is "<literal>app</literal>" and @action_group
|
|
* contains an action called "<literal>quit</literal>", then @muxer will
|
|
* now contain an action called "<literal>app.quit</literal>".
|
|
*
|
|
* If any #GActionObservers are registered for actions in the group,
|
|
* "action_added" notifications will be emitted, as appropriate.
|
|
*
|
|
* @prefix must not contain a dot ('.').
|
|
*/
|
|
void
|
|
g_action_muxer_insert (GActionMuxer *muxer,
|
|
const gchar *prefix,
|
|
GActionGroup *action_group)
|
|
{
|
|
gchar **actions;
|
|
Group *group;
|
|
gint i;
|
|
|
|
/* TODO: diff instead of ripout and replace */
|
|
g_action_muxer_remove (muxer, prefix);
|
|
|
|
group = g_slice_new (Group);
|
|
group->muxer = muxer;
|
|
group->group = g_object_ref (action_group);
|
|
group->prefix = g_strdup (prefix);
|
|
|
|
g_hash_table_insert (muxer->groups, group->prefix, group);
|
|
|
|
actions = g_action_group_list_actions (group->group);
|
|
for (i = 0; actions[i]; i++)
|
|
g_action_muxer_action_added (group->group, actions[i], group);
|
|
g_strfreev (actions);
|
|
|
|
group->handler_ids[0] = g_signal_connect (group->group, "action-added",
|
|
G_CALLBACK (g_action_muxer_action_added), group);
|
|
group->handler_ids[1] = g_signal_connect (group->group, "action-removed",
|
|
G_CALLBACK (g_action_muxer_action_removed), group);
|
|
group->handler_ids[2] = g_signal_connect (group->group, "action-enabled-changed",
|
|
G_CALLBACK (g_action_muxer_action_enabled_changed), group);
|
|
group->handler_ids[3] = g_signal_connect (group->group, "action-state-changed",
|
|
G_CALLBACK (g_action_muxer_action_state_changed), group);
|
|
}
|
|
|
|
/*
|
|
* g_action_muxer_remove:
|
|
* @muxer: a #GActionMuxer
|
|
* @prefix: the prefix of the action group to remove
|
|
*
|
|
* Removes a #GActionGroup from the #GActionMuxer.
|
|
*
|
|
* If any #GActionObservers are registered for actions in the group,
|
|
* "action_removed" notifications will be emitted, as appropriate.
|
|
*/
|
|
void
|
|
g_action_muxer_remove (GActionMuxer *muxer,
|
|
const gchar *prefix)
|
|
{
|
|
Group *group;
|
|
|
|
group = g_hash_table_lookup (muxer->groups, prefix);
|
|
|
|
if (group != NULL)
|
|
{
|
|
gchar **actions;
|
|
gint i;
|
|
|
|
g_hash_table_steal (muxer->groups, prefix);
|
|
|
|
actions = g_action_group_list_actions (group->group);
|
|
for (i = 0; actions[i]; i++)
|
|
g_action_muxer_action_removed (group->group, actions[i], group);
|
|
g_strfreev (actions);
|
|
|
|
g_action_muxer_free_group (group);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* g_action_muxer_new:
|
|
*
|
|
* Creates a new #GActionMuxer.
|
|
*/
|
|
GActionMuxer *
|
|
g_action_muxer_new (void)
|
|
{
|
|
return g_object_new (G_TYPE_ACTION_MUXER, NULL);
|
|
}
|