diff --git a/gtk/Makefile.am b/gtk/Makefile.am index 397fa4fa25..2dec8dbc6d 100644 --- a/gtk/Makefile.am +++ b/gtk/Makefile.am @@ -407,6 +407,7 @@ gtk_private_h_sources = \ gtkapplicationprivate.h \ gtkaccelgroupprivate.h \ gtkaccelmapprivate.h \ + gtkactionhelper.h \ gtkallocatedbitmaskprivate.h \ gtkappchooserprivate.h \ gtkappchoosermodule.h \ @@ -586,6 +587,7 @@ gtk_base_c_sources = \ gtkaccelmap.c \ gtkaccessible.c \ gtkaction.c \ + gtkactionhelper.c \ gtkactiongroup.c \ gtkactivatable.c \ gtkadjustment.c \ diff --git a/gtk/gtkactionhelper.c b/gtk/gtkactionhelper.c new file mode 100644 index 0000000000..6047a43cf9 --- /dev/null +++ b/gtk/gtkactionhelper.c @@ -0,0 +1,631 @@ +/* + * Copyright © 2012 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, see . + * + * Authors: Ryan Lortie + */ + +#include "gtkactionhelper.h" +#include "gactionobservable.h" + +#include "gtkwidget.h" +#include "gtkwidgetprivate.h" + +#include + +typedef struct +{ + GActionGroup *group; + + GHashTable *watchers; +} GtkActionHelperGroup; + +static void gtk_action_helper_action_added (GtkActionHelper *helper, + gboolean enabled, + const GVariantType *parameter_type, + GVariant *state, + gboolean should_emit_signals); + +static void gtk_action_helper_action_removed (GtkActionHelper *helper); + +static void gtk_action_helper_action_enabled_changed (GtkActionHelper *helper, + gboolean enabled); + +static void gtk_action_helper_action_state_changed (GtkActionHelper *helper, + GVariant *new_state); + +typedef GObjectClass GtkActionHelperClass; + +struct _GtkActionHelper +{ + GObject parent_instance; + + GtkApplication *application; + GtkWidget *widget; + + GtkActionHelperGroup *group; + + GActionMuxer *action_context; + gchar *action_name; + + GVariant *target; + + GtkActionHelperRole role; + gboolean can_activate; + gboolean enabled; + gboolean active; + + gint reporting; +}; + +enum +{ + PROP_0, + PROP_ENABLED, + PROP_ACTIVE, + PROP_ROLE, + N_PROPS +}; + +static GParamSpec *gtk_action_helper_pspecs[N_PROPS]; + +static void gtk_action_helper_observer_iface_init (GActionObserverInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (GtkActionHelper, gtk_action_helper, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_OBSERVER, gtk_action_helper_observer_iface_init)) + +static void +gtk_action_helper_report_change (GtkActionHelper *helper, + guint prop_id) +{ + helper->reporting++; + + if (!helper->application) + { + switch (prop_id) + { + case PROP_ENABLED: + gtk_widget_set_sensitive (GTK_WIDGET (helper->widget), helper->enabled); + break; + + case PROP_ACTIVE: + { + GParamSpec *pspec; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), "active"); + + if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN) + g_object_set (G_OBJECT (helper->widget), "active", helper->active, NULL); + } + break; + + case PROP_ROLE: + { + GParamSpec *pspec; + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), "action-role"); + + if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_UINT) + g_object_set (G_OBJECT (helper->widget), "action-role", helper->role, NULL); + } + break; + + default: + g_assert_not_reached (); + } + } + + g_object_notify_by_pspec (G_OBJECT (helper), gtk_action_helper_pspecs[prop_id]); + helper->reporting--; +} + +static void +gtk_action_helper_action_added (GtkActionHelper *helper, + gboolean enabled, + const GVariantType *parameter_type, + GVariant *state, + gboolean should_emit_signals) +{ + /* we can only activate if we have the correct type of parameter */ + helper->can_activate = (helper->target == NULL && parameter_type == NULL) || + (helper->target != NULL && parameter_type != NULL && + g_variant_is_of_type (helper->target, parameter_type)); + + if (!helper->can_activate) + return; + + helper->enabled = enabled; + + if (helper->target != NULL && state != NULL) + { + helper->active = g_variant_equal (state, helper->target); + helper->role = GTK_ACTION_HELPER_ROLE_RADIO; + } + + else if (state != NULL && g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN)) + { + helper->active = g_variant_get_boolean (state); + helper->role = GTK_ACTION_HELPER_ROLE_TOGGLE; + } + + if (should_emit_signals) + { + if (helper->enabled) + gtk_action_helper_report_change (helper, PROP_ENABLED); + + if (helper->active) + gtk_action_helper_report_change (helper, PROP_ACTIVE); + + if (helper->role) + gtk_action_helper_report_change (helper, PROP_ROLE); + } +} + +static void +gtk_action_helper_action_removed (GtkActionHelper *helper) +{ + if (!helper->can_activate) + return; + + helper->can_activate = FALSE; + + if (helper->enabled) + { + helper->enabled = FALSE; + gtk_action_helper_report_change (helper, PROP_ENABLED); + } + + if (helper->active) + { + helper->enabled = FALSE; + gtk_action_helper_report_change (helper, PROP_ACTIVE); + } + + if (helper->role) + { + helper->role = GTK_ACTION_HELPER_ROLE_NORMAL; + gtk_action_helper_report_change (helper, PROP_ROLE); + } +} + +static void +gtk_action_helper_action_enabled_changed (GtkActionHelper *helper, + gboolean enabled) +{ + if (!helper->can_activate) + return; + + if (helper->enabled == enabled) + return; + + helper->enabled = enabled; + gtk_action_helper_report_change (helper, PROP_ENABLED); +} + +static void +gtk_action_helper_action_state_changed (GtkActionHelper *helper, + GVariant *new_state) +{ + gboolean was_active; + + if (!helper->can_activate) + return; + + was_active = helper->active; + + if (helper->target) + helper->active = g_variant_equal (new_state, helper->target); + + else if (g_variant_is_of_type (new_state, G_VARIANT_TYPE_BOOLEAN)) + helper->active = g_variant_get_boolean (new_state); + + else + helper->active = FALSE; + + if (helper->active != was_active) + gtk_action_helper_report_change (helper, PROP_ACTIVE); +} + +static void +gtk_action_helper_get_property (GObject *object, guint prop_id, + GValue *value, GParamSpec *pspec) +{ + GtkActionHelper *helper = GTK_ACTION_HELPER (object); + + switch (prop_id) + { + case PROP_ENABLED: + g_value_set_boolean (value, helper->enabled); + break; + + case PROP_ACTIVE: + g_value_set_boolean (value, helper->active); + break; + + case PROP_ROLE: + g_value_set_uint (value, helper->role); + break; + + default: + g_assert_not_reached (); + } +} + +static void +gtk_action_helper_finalize (GObject *object) +{ + GtkActionHelper *helper = GTK_ACTION_HELPER (object); + + if (helper->application) + { + g_signal_handlers_disconnect_by_data (helper->application, helper); + g_object_unref (helper->action_context); + g_object_unref (helper->application); + + if (helper->widget) + g_object_unref (helper->widget); + } + + g_free (helper->action_name); + + if (helper->target) + g_variant_unref (helper->target); + + G_OBJECT_CLASS (gtk_action_helper_parent_class) + ->finalize (object); +} + +static void +gtk_action_helper_observer_action_added (GActionObserver *observer, + GActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state) +{ + gtk_action_helper_action_added (GTK_ACTION_HELPER (observer), enabled, parameter_type, state, TRUE); +} + +static void +gtk_action_helper_observer_action_enabled_changed (GActionObserver *observer, + GActionObservable *observable, + const gchar *action_name, + gboolean enabled) +{ + gtk_action_helper_action_enabled_changed (GTK_ACTION_HELPER (observer), enabled); +} + +static void +gtk_action_helper_observer_action_state_changed (GActionObserver *observer, + GActionObservable *observable, + const gchar *action_name, + GVariant *state) +{ + gtk_action_helper_action_state_changed (GTK_ACTION_HELPER (observer), state); +} + +static void +gtk_action_helper_observer_action_removed (GActionObserver *observer, + GActionObservable *observable, + const gchar *action_name) +{ + gtk_action_helper_action_removed (GTK_ACTION_HELPER (observer)); +} + +static void +gtk_action_helper_init (GtkActionHelper *helper) +{ +} + +static void +gtk_action_helper_class_init (GtkActionHelperClass *class) +{ + class->get_property = gtk_action_helper_get_property; + class->finalize = gtk_action_helper_finalize; + + gtk_action_helper_pspecs[PROP_ENABLED] = g_param_spec_boolean ("enabled", "enabled", "enabled", FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + gtk_action_helper_pspecs[PROP_ACTIVE] = g_param_spec_boolean ("active", "active", "active", FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + gtk_action_helper_pspecs[PROP_ROLE] = g_param_spec_uint ("role", "role", "role", 0, 2, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (class, N_PROPS, gtk_action_helper_pspecs); +} + +static void +gtk_action_helper_observer_iface_init (GActionObserverInterface *iface) +{ + iface->action_added = gtk_action_helper_observer_action_added; + iface->action_enabled_changed = gtk_action_helper_observer_action_enabled_changed; + iface->action_state_changed = gtk_action_helper_observer_action_state_changed; + iface->action_removed = gtk_action_helper_observer_action_removed; +} + +/*< private > + * gtk_action_helper_new: + * @widget: a #GtkWidget implementing #GtkActionable + * + * Creates a helper to track the state of a named action. This will + * usually be used by widgets implementing #GtkActionable. + * + * This helper class is usually used by @widget itself. In order to + * avoid reference cycles, the helper does not hold a reference on + * @widget, but will assume that it continues to exist for the duration + * of the life of the helper. If you are using the helper from outside + * of the widget, you should take a ref on @widget for each ref you hold + * on the helper. + * + * Returns: a new #GtkActionHelper + */ +GtkActionHelper * +gtk_action_helper_new (GtkActionable *widget) +{ + GtkActionHelper *helper; + + g_return_val_if_fail (GTK_IS_ACTIONABLE (widget), NULL); + helper = g_object_new (GTK_TYPE_ACTION_HELPER, NULL); + + helper->widget = GTK_WIDGET (widget); + + if (helper->widget) + { + GParamSpec *pspec; + + helper->enabled = gtk_widget_get_sensitive (GTK_WIDGET (helper->widget)); + + pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (helper->widget), "active"); + if (pspec && G_PARAM_SPEC_VALUE_TYPE (pspec) == G_TYPE_BOOLEAN) + g_object_get (G_OBJECT (helper->widget), "active", &helper->active, NULL); + } + + helper->action_context = _gtk_widget_get_action_muxer (GTK_WIDGET (widget)); + + return helper; +} + +static void +gtk_action_helper_active_window_changed (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GtkActionHelper *helper = user_data; + GActionMuxer *parent; + + if (helper->widget) + g_object_unref (helper->widget); + + helper->widget = GTK_WIDGET (gtk_application_get_active_window (helper->application)); + + if (helper->widget) + { + parent = g_object_ref (_gtk_widget_get_action_muxer (GTK_WIDGET (helper->widget))); + g_object_ref (helper->widget); + } + else + { + parent = g_action_muxer_new (); + g_action_muxer_insert (parent, "app", G_ACTION_GROUP (helper->application)); + } + + g_action_muxer_set_parent (helper->action_context, parent); + g_object_unref (parent); +} + +GtkActionHelper * +gtk_action_helper_new_with_application (GtkApplication *application) +{ + GtkActionHelper *helper; + + g_return_val_if_fail (GTK_IS_APPLICATION (application), NULL); + + helper = g_object_new (GTK_TYPE_ACTION_HELPER, NULL); + helper->application = g_object_ref (application); + + helper->action_context = g_action_muxer_new (); + g_signal_connect (application, "notify::active-window", G_CALLBACK (gtk_action_helper_active_window_changed), helper); + gtk_action_helper_active_window_changed (NULL, NULL, helper); + + return helper; +} + +void +gtk_action_helper_set_action_name (GtkActionHelper *helper, + const gchar *action_name) +{ + gboolean was_enabled, was_active; + GtkActionHelperRole old_role; + const GVariantType *parameter_type; + gboolean enabled; + GVariant *state; + + if (g_strcmp0 (action_name, helper->action_name) == 0) + return; + + if (helper->action_name) + { + g_action_observable_unregister_observer (G_ACTION_OBSERVABLE (helper->action_context), + helper->action_name, + G_ACTION_OBSERVER (helper)); + g_free (helper->action_name); + } + + helper->action_name = g_strdup (action_name); + + g_action_observable_register_observer (G_ACTION_OBSERVABLE (helper->action_context), + helper->action_name, + G_ACTION_OBSERVER (helper)); + + /* Start by recording the current state of our properties so we know + * what notify signals we will need to send. + */ + was_enabled = helper->enabled; + was_active = helper->active; + old_role = helper->role; + + if (g_action_group_query_action (G_ACTION_GROUP (helper->action_context), helper->action_name, + &enabled, ¶meter_type, NULL, NULL, &state)) + { + gtk_action_helper_action_added (helper, enabled, parameter_type, state, FALSE); + + if (state) + g_variant_unref (state); + } + else + { + helper->enabled = FALSE; + } + + /* Send the notifies for the properties that changed. + * + * When called during construction, widget is NULL. We don't need to + * report in that case. + */ + if (helper->enabled != was_enabled) + gtk_action_helper_report_change (helper, PROP_ENABLED); + + if (helper->active != was_active) + gtk_action_helper_report_change (helper, PROP_ACTIVE); + + if (helper->role != old_role) + gtk_action_helper_report_change (helper, PROP_ROLE); + + if (!helper->application) + g_object_notify (G_OBJECT (helper->widget), "action-name"); +} + +/*< private > + * gtk_action_helper_set_action_target_value: + * @helper: a #GtkActionHelper + * @target_value: an action target, as per #GtkActionable + * + * This function consumes @action_target if it is floating. + */ +void +gtk_action_helper_set_action_target_value (GtkActionHelper *helper, + GVariant *target_value) +{ + gboolean was_enabled; + gboolean was_active; + + if (target_value == helper->target) + return; + + if (target_value && helper->target && g_variant_equal (target_value, helper->target)) + return; + + if (helper->target) + { + g_variant_unref (helper->target); + helper->target = NULL; + } + + if (target_value) + helper->target = g_variant_ref_sink (target_value); + + was_enabled = helper->enabled; + was_active = helper->active; + + /* If we are attached to an action group then it is possible that this + * change of the target value could impact our properties (including + * changes to 'can_activate' and therefore 'enabled', due to resolving + * a parameter type mismatch). + * + * Start over again by pretending the action gets re-added. + */ + helper->can_activate = FALSE; + helper->enabled = FALSE; + helper->active = FALSE; + + if (helper->action_context) + { + const GVariantType *parameter_type; + gboolean enabled; + GVariant *state; + + if (g_action_group_query_action (G_ACTION_GROUP (helper->action_context), + helper->action_name, &enabled, ¶meter_type, + NULL, NULL, &state)) + { + gtk_action_helper_action_added (helper, enabled, parameter_type, state, FALSE); + + if (state) + g_variant_unref (state); + } + } + + if (helper->enabled != was_enabled) + gtk_action_helper_report_change (helper, PROP_ENABLED); + + if (helper->active != was_active) + gtk_action_helper_report_change (helper, PROP_ACTIVE); + + if (!helper->application) + g_object_notify (G_OBJECT (helper->widget), "action-target"); +} + +const gchar * +gtk_action_helper_get_action_name (GtkActionHelper *helper) +{ + if (helper == NULL) + return NULL; + + return helper->action_name; +} + +GVariant * +gtk_action_helper_get_action_target_value (GtkActionHelper *helper) +{ + if (helper == NULL) + return NULL; + + return helper->target; +} + +GtkActionHelperRole +gtk_action_helper_get_role (GtkActionHelper *helper) +{ + if (helper == NULL) + return GTK_ACTION_HELPER_ROLE_NORMAL; + + return helper->role; +} + +gboolean +gtk_action_helper_get_enabled (GtkActionHelper *helper) +{ + g_return_val_if_fail (GTK_IS_ACTION_HELPER (helper), FALSE); + + return helper->enabled; +} + +gboolean +gtk_action_helper_get_active (GtkActionHelper *helper) +{ + g_return_val_if_fail (GTK_IS_ACTION_HELPER (helper), FALSE); + + return helper->active; +} + +void +gtk_action_helper_activate (GtkActionHelper *helper) +{ + g_return_if_fail (GTK_IS_ACTION_HELPER (helper)); + + if (!helper->can_activate || helper->reporting) + return; + + g_action_group_activate_action (G_ACTION_GROUP (helper->action_context), + helper->action_name, helper->target); +} diff --git a/gtk/gtkactionhelper.h b/gtk/gtkactionhelper.h new file mode 100644 index 0000000000..bc54357dd9 --- /dev/null +++ b/gtk/gtkactionhelper.h @@ -0,0 +1,71 @@ +/* + * Copyright © 2012 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, see . + * + * Authors: Ryan Lortie + */ + +#ifndef __GTK_ACTION_HELPER_H__ +#define __GTK_ACTION_HELPER_H__ + +#include +#include + +#define GTK_TYPE_ACTION_HELPER (gtk_action_helper_get_type ()) +#define GTK_ACTION_HELPER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_HELPER, GtkActionHelper)) +#define GTK_IS_ACTION_HELPER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_HELPER)) + +typedef struct _GtkActionHelper GtkActionHelper; + +typedef enum +{ + GTK_ACTION_HELPER_ROLE_NORMAL, + GTK_ACTION_HELPER_ROLE_TOGGLE, + GTK_ACTION_HELPER_ROLE_RADIO +} GtkActionHelperRole; + +G_GNUC_INTERNAL +GType gtk_action_helper_get_type (void); + +G_GNUC_INTERNAL +GtkActionHelper * gtk_action_helper_new (GtkActionable *widget); + +G_GNUC_INTERNAL +GtkActionHelper * gtk_action_helper_new_with_application (GtkApplication *application); + +G_GNUC_INTERNAL +void gtk_action_helper_set_action_name (GtkActionHelper *helper, + const gchar *action_name); +G_GNUC_INTERNAL +void gtk_action_helper_set_action_target_value (GtkActionHelper *helper, + GVariant *action_target); +G_GNUC_INTERNAL +const gchar * gtk_action_helper_get_action_name (GtkActionHelper *helper); +G_GNUC_INTERNAL +GVariant * gtk_action_helper_get_action_target_value (GtkActionHelper *helper); + +G_GNUC_INTERNAL +GtkActionHelperRole gtk_action_helper_get_role (GtkActionHelper *helper); +G_GNUC_INTERNAL +gboolean gtk_action_helper_get_enabled (GtkActionHelper *helper); +G_GNUC_INTERNAL +gboolean gtk_action_helper_get_active (GtkActionHelper *helper); + +G_GNUC_INTERNAL +void gtk_action_helper_activate (GtkActionHelper *helper); + +#endif /* __GTK_ACTION_HELPER_H__ */