/* * 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->active = 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)) { g_variant_unref (g_variant_ref_sink (target_value)); return; } if (helper->target) { g_variant_unref (helper->target); helper->target = NULL; } if (target_value) helper->target = g_variant_ref_sink (target_value); /* The action_name has not yet been set. Don't do anything yet. */ if (helper->action_name == NULL) return; 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); }