/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikaaction.c * Copyright (C) 2004-2019 Michael Natterer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include "libpikabase/pikabase.h" #include "libpikacolor/pikacolor.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "config/pikacoreconfig.h" #include "core/pika.h" #include "core/pikacontext.h" #include "core/pikaimagefile.h" /* eek */ #include "pikaaction.h" #include "pikaprocedureaction.h" #include "pikaview.h" #include "pikaviewrenderer.h" #include "pikawidgets-utils.h" enum { ACTIVATE, ACCELS_CHANGED, LAST_SIGNAL }; #define GET_PRIVATE(obj) (pika_action_get_private ((PikaAction *) (obj))) typedef struct _PikaActionPrivate PikaActionPrivate; struct _PikaActionPrivate { PikaContext *context; /* This recursive pointer is needed for the finalize(). */ PikaAction *action; PikaActionGroup *group; gboolean sensitive; gchar *disable_reason; gboolean visible; gchar *label; gchar *short_label; gchar *tooltip; gchar *icon_name; GIcon *icon; gchar **accels; gchar **default_accels; gchar *menu_path; PikaRGB *color; PikaViewable *viewable; PangoEllipsizeMode ellipsize; gint max_width_chars; GList *proxies; }; static PikaActionPrivate * pika_action_get_private (PikaAction *action); static void pika_action_private_finalize (PikaActionPrivate *priv); static void pika_action_label_notify (PikaAction *action, const GParamSpec *pspec, gpointer data); static void pika_action_proxy_destroy (GtkWidget *proxy, PikaAction *action); static void pika_action_proxy_button_activate (GtkButton *button, PikaAction *action); static void pika_action_update_proxy_sensitive (PikaAction *action, GtkWidget *proxy); static void pika_action_update_proxy_tooltip (PikaAction *action, GtkWidget *proxy); G_DEFINE_INTERFACE (PikaAction, pika_action, PIKA_TYPE_OBJECT) static guint action_signals[LAST_SIGNAL]; static void pika_action_default_init (PikaActionInterface *iface) { PikaRGB black; action_signals[ACTIVATE] = g_signal_new ("activate", G_TYPE_FROM_INTERFACE (iface), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (PikaActionInterface, activate), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_VARIANT); action_signals[ACCELS_CHANGED] = g_signal_new ("accels-changed", G_TYPE_FROM_INTERFACE (iface), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (PikaActionInterface, accels_changed), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRV); g_object_interface_install_property (iface, g_param_spec_object ("context", NULL, NULL, PIKA_TYPE_CONTEXT, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); g_object_interface_install_property (iface, g_param_spec_boolean ("sensitive", NULL, NULL, TRUE, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_interface_install_property (iface, g_param_spec_boolean ("visible", NULL, NULL, TRUE, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_interface_install_property (iface, g_param_spec_string ("label", NULL, NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_interface_install_property (iface, g_param_spec_string ("short-label", NULL, NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_interface_install_property (iface, g_param_spec_string ("tooltip", NULL, NULL, NULL, PIKA_PARAM_READWRITE)); g_object_interface_install_property (iface, g_param_spec_string ("icon-name", NULL, NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_interface_install_property (iface, g_param_spec_object ("icon", NULL, NULL, G_TYPE_ICON, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); pika_rgba_set (&black, 0.0, 0.0, 0.0, PIKA_OPACITY_OPAQUE); g_object_interface_install_property (iface, pika_param_spec_rgb ("color", NULL, NULL, TRUE, &black, PIKA_PARAM_READWRITE)); g_object_interface_install_property (iface, g_param_spec_object ("viewable", NULL, NULL, PIKA_TYPE_VIEWABLE, PIKA_PARAM_READWRITE)); g_object_interface_install_property (iface, g_param_spec_enum ("ellipsize", NULL, NULL, PANGO_TYPE_ELLIPSIZE_MODE, PANGO_ELLIPSIZE_NONE, PIKA_PARAM_READWRITE)); g_object_interface_install_property (iface, g_param_spec_int ("max-width-chars", NULL, NULL, -1, G_MAXINT, -1, PIKA_PARAM_READWRITE)); } void pika_action_init (PikaAction *action) { PikaActionPrivate *priv; g_return_if_fail (PIKA_IS_ACTION (action)); priv = GET_PRIVATE (action); priv->action = action; priv->group = NULL; priv->sensitive = TRUE; priv->visible = TRUE; priv->label = NULL; priv->short_label = NULL; priv->tooltip = NULL; priv->icon_name = NULL; priv->icon = NULL; priv->accels = NULL; priv->default_accels = NULL; priv->menu_path = NULL; priv->ellipsize = PANGO_ELLIPSIZE_NONE; priv->max_width_chars = -1; priv->proxies = NULL; g_signal_connect (action, "notify::label", G_CALLBACK (pika_action_label_notify), NULL); g_signal_connect (action, "notify::short-label", G_CALLBACK (pika_action_label_notify), NULL); } /* public functions */ void pika_action_emit_activate (PikaAction *action, GVariant *value) { g_return_if_fail (PIKA_IS_ACTION (action)); if (value) g_variant_ref_sink (value); g_signal_emit (action, action_signals[ACTIVATE], 0, value); if (value) g_variant_unref (value); } void pika_action_emit_change_state (PikaAction *action, GVariant *value) { g_return_if_fail (PIKA_IS_ACTION (action)); if (value) g_variant_ref_sink (value); g_signal_emit_by_name (action, "change-state", value); if (value) g_variant_unref (value); } const gchar * pika_action_get_name (PikaAction *action) { return pika_object_get_name (PIKA_OBJECT (action)); } PikaActionGroup * pika_action_get_group (PikaAction *action) { return GET_PRIVATE (action)->group; } void pika_action_set_label (PikaAction *action, const gchar *label) { PikaActionPrivate *priv = GET_PRIVATE (action); if (g_strcmp0 (priv->label, label) != 0) { g_free (priv->label); priv->label = g_strdup (label); /* Set or update the proxy rendering. */ for (GList *list = priv->proxies; list; list = list->next) pika_action_set_proxy (action, list->data); g_object_notify (G_OBJECT (action), "label"); if (priv->short_label == NULL) g_object_notify (G_OBJECT (action), "short-label"); } } const gchar * pika_action_get_label (PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); return priv->label; } void pika_action_set_short_label (PikaAction *action, const gchar *label) { PikaActionPrivate *priv = GET_PRIVATE (action); if (g_strcmp0 (priv->short_label, label) != 0) { g_free (priv->short_label); priv->short_label = g_strdup (label); /* Set or update the proxy rendering. */ for (GList *list = priv->proxies; list; list = list->next) pika_action_set_proxy (action, list->data); g_object_notify (G_OBJECT (action), "short-label"); } } const gchar * pika_action_get_short_label (PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); return priv->short_label ? priv->short_label : priv->label; } void pika_action_set_tooltip (PikaAction *action, const gchar *tooltip) { PikaActionPrivate *priv = GET_PRIVATE (action); if (g_strcmp0 (priv->tooltip, tooltip) != 0) { g_free (priv->tooltip); priv->tooltip = g_strdup (tooltip); pika_action_update_proxy_tooltip (action, NULL); g_object_notify (G_OBJECT (action), "tooltip"); } } const gchar * pika_action_get_tooltip (PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); return priv->tooltip; } void pika_action_set_icon_name (PikaAction *action, const gchar *icon_name) { PikaActionPrivate *priv = GET_PRIVATE (action); if (g_strcmp0 (priv->icon_name, icon_name) != 0) { g_free (priv->icon_name); priv->icon_name = g_strdup (icon_name); /* Set or update the proxy rendering. */ for (GList *list = priv->proxies; list; list = list->next) pika_action_set_proxy (action, list->data); g_object_notify (G_OBJECT (action), "icon-name"); } } const gchar * pika_action_get_icon_name (PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); return priv->icon_name; } void pika_action_set_gicon (PikaAction *action, GIcon *icon) { PikaActionPrivate *priv = GET_PRIVATE (action); if (priv->icon != icon) { g_clear_object (&priv->icon); priv->icon = g_object_ref (icon); /* Set or update the proxy rendering. */ for (GList *list = priv->proxies; list; list = list->next) pika_action_set_proxy (action, list->data); g_object_notify (G_OBJECT (action), "icon"); } } GIcon * pika_action_get_gicon (PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); return priv->icon ? g_object_ref (priv->icon) : NULL; } void pika_action_set_help_id (PikaAction *action, const gchar *help_id) { g_return_if_fail (PIKA_IS_ACTION (action)); g_object_set_qdata_full (G_OBJECT (action), PIKA_HELP_ID, g_strdup (help_id), (GDestroyNotify) g_free); pika_action_update_proxy_tooltip (action, NULL); } const gchar * pika_action_get_help_id (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return g_object_get_qdata (G_OBJECT (action), PIKA_HELP_ID); } void pika_action_set_visible (PikaAction *action, gboolean visible) { g_object_set (action, "visible", visible, NULL); } gboolean pika_action_get_visible (PikaAction *action) { return GET_PRIVATE (action)->visible; } gboolean pika_action_is_visible (PikaAction *action) { gboolean visible; visible = pika_action_get_visible (action); if (visible) { /* TODO: check if the action group itself is visible. * See implementation of gtk_action_is_visible(). */ } return visible; } void pika_action_set_sensitive (PikaAction *action, gboolean sensitive, const gchar *reason) { PikaActionPrivate *priv = GET_PRIVATE (action); if (priv->sensitive != sensitive || (! sensitive && g_strcmp0 (reason, priv->disable_reason) != 0)) { priv->sensitive = sensitive; g_clear_pointer (&priv->disable_reason, g_free); if (reason && ! sensitive) priv->disable_reason = g_strdup (reason); g_object_notify (G_OBJECT (action), "sensitive"); pika_action_update_proxy_sensitive (action, NULL); g_object_notify (G_OBJECT (action), "enabled"); } } gboolean pika_action_get_sensitive (PikaAction *action, const gchar **reason) { PikaActionPrivate *priv = GET_PRIVATE (action); gboolean sensitive; sensitive = priv->sensitive; if (reason) { PikaActionPrivate *priv = GET_PRIVATE (action); *reason = NULL; if (! sensitive && priv->disable_reason != NULL) *reason = (const gchar *) priv->disable_reason; } return sensitive; } gboolean pika_action_is_sensitive (PikaAction *action, const gchar **reason) { gboolean sensitive; sensitive = pika_action_get_sensitive (action, reason); if (sensitive) { /* TODO: check if the action group itself is sensitive. * See implementation of gtk_action_is_sensitive(). */ } return sensitive; } /** * pika_action_set_accels: * @action: a #PikaAction * @accels: accelerators in the format understood by gtk_accelerator_parse(). * The first accelerator is the main one. * * Set the accelerators to be associated with the given @action. * * Note that the #PikaAction API will emit the signal "accels-changed" whereas * GtkApplication has no signal (that we could find) to connect to. It means we * must always change accelerators with this function. * Never use gtk_application_set_accels_for_action() directly! */ void pika_action_set_accels (PikaAction *action, const gchar **accels) { PikaActionPrivate *priv = GET_PRIVATE (action); g_return_if_fail (PIKA_IS_ACTION (action)); if (accels != NULL && priv->accels != NULL && g_strv_equal (accels, (const gchar **) priv->accels)) return; g_strfreev (priv->accels); priv->accels = g_strdupv ((gchar **) accels); g_signal_emit (action, action_signals[ACCELS_CHANGED], 0, accels); } /** * pika_action_get_accels: * @action: a #PikaAction * * Gets the accelerators that are currently associated with * the given @action. * * Returns: (transfer full): accelerators for @action, as a %NULL-terminated * array. Free with g_strfreev() when no longer needed */ const gchar ** pika_action_get_accels (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return (const gchar **) GET_PRIVATE (action)->accels; } /** * pika_action_get_default_accels: * @action: a #PikaAction * * Gets the accelerators that are associated with the given @action by default. * These might be different from pika_action_get_accels(). * * Returns: (transfer full): default accelerators for @action, as a * %NULL-terminated array. Free with g_strfreev() when * no longer needed */ const gchar ** pika_action_get_default_accels (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return (const gchar **) GET_PRIVATE (action)->default_accels; } /** * pika_action_get_display_accels: * @action: a #PikaAction * * Gets the accelerators that are currently associated with the given @action, * in a format which can be presented to people on the GUI. * * Returns: (transfer full): accelerators for @action, as a %NULL-terminated * array. Free with g_strfreev() when no longer needed */ gchar ** pika_action_get_display_accels (PikaAction *action) { gchar **accels; gint i; g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); accels = g_strdupv (GET_PRIVATE (action)->accels); for (i = 0; accels != NULL && accels[i] != NULL; i++) { guint accel_key = 0; GdkModifierType accel_mods = 0; gtk_accelerator_parse (accels[i], &accel_key, &accel_mods); if (accel_key != 0 || accel_mods != 0) { gchar *accel; accel = gtk_accelerator_get_label (accel_key, accel_mods); g_free (accels[i]); accels[i] = accel; } } return accels; } /* This should return FALSE if the currently set accelerators are not the * default accelerators or even if they are in different order (different * primary accelerator in particular). */ gboolean pika_action_use_default_accels (PikaAction *action) { gchar **default_accels; gchar **accels; g_return_val_if_fail (PIKA_IS_ACTION (action), TRUE); default_accels = GET_PRIVATE (action)->default_accels; accels = GET_PRIVATE (action)->accels; if ((default_accels == NULL || g_strv_length (default_accels) == 0) && (accels == NULL || g_strv_length (accels) == 0)) return TRUE; if (default_accels == NULL || accels == NULL || g_strv_length (default_accels) != g_strv_length (accels)) return FALSE; /* An easy looking variant would be to simply compare with g_strv_equal() but * this actually doesn't work because gtk_accelerator_parse() is liberal with * the format (casing and abbreviations) and thus we need to have a canonical * string version, or simply parse the accelerators down to get to the base * key/modes, which is what I do here. */ for (gint i = 0; accels[i] != NULL; i++) { guint default_key; GdkModifierType default_mods; guint accelerator_key; GdkModifierType accelerator_mods; gtk_accelerator_parse (default_accels[i], &default_key, &default_mods); gtk_accelerator_parse (accels[i], &accelerator_key, &accelerator_mods); if (default_key != accelerator_key || default_mods != accelerator_mods) return FALSE; } return TRUE; } const gchar * pika_action_get_menu_path (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return GET_PRIVATE (action)->menu_path; } void pika_action_activate (PikaAction *action) { g_return_if_fail (G_IS_ACTION (action)); g_action_activate (G_ACTION (action), NULL); } gint pika_action_name_compare (PikaAction *action1, PikaAction *action2) { return strcmp (pika_action_get_name (action1), pika_action_get_name (action2)); } gboolean pika_action_is_gui_blacklisted (const gchar *action_name) { static const gchar *prefixes[] = { "<", "tools-color-average-radius-", "tools-paintbrush-size-", "tools-paintbrush-aspect-ratio-", "tools-paintbrush-angle-", "tools-paintbrush-spacing-", "tools-paintbrush-hardness-", "tools-paintbrush-force-", "tools-ink-blob-size-", "tools-ink-blob-aspect-", "tools-ink-blob-angle-", "tools-mypaint-brush-radius-", "tools-mypaint-brush-hardness-", "tools-foreground-select-brush-size-", "tools-transform-preview-opacity-", "tools-warp-effect-size-", "tools-warp-effect-hardness-" }; static const gchar *actions[] = { "tools-brightness-contrast", "tools-curves", "tools-levels", "tools-offset", "tools-threshold", "layers-mask-add-button" }; gint i; if (! (action_name && *action_name)) return TRUE; for (i = 0; i < G_N_ELEMENTS (prefixes); i++) { if (g_str_has_prefix (action_name, prefixes[i])) return TRUE; } for (i = 0; i < G_N_ELEMENTS (actions); i++) { if (! strcmp (action_name, actions[i])) return TRUE; } return FALSE; } PikaViewable * pika_action_get_viewable (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return GET_PRIVATE (action)->viewable; } PikaContext * pika_action_get_context (PikaAction *action) { g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); return GET_PRIVATE (action)->context; } /* Protected functions. */ /** * pika_action_install_properties: * @klass: the class structure for a type deriving from #GObject * * Installs the necessary properties for a class implementing * #PikaAction. Please call this function in the *_class_init() * function of the child class. **/ void pika_action_install_properties (GObjectClass *klass) { g_object_class_override_property (klass, PIKA_ACTION_PROP_CONTEXT, "context"); g_object_class_override_property (klass, PIKA_ACTION_PROP_SENSITIVE, "sensitive"); g_object_class_override_property (klass, PIKA_ACTION_PROP_VISIBLE, "visible"); g_object_class_override_property (klass, PIKA_ACTION_PROP_LABEL, "label"); g_object_class_override_property (klass, PIKA_ACTION_PROP_SHORT_LABEL, "short-label"); g_object_class_override_property (klass, PIKA_ACTION_PROP_TOOLTIP, "tooltip"); g_object_class_override_property (klass, PIKA_ACTION_PROP_ICON_NAME, "icon-name"); g_object_class_override_property (klass, PIKA_ACTION_PROP_ICON, "icon"); g_object_class_override_property (klass, PIKA_ACTION_PROP_COLOR, "color"); g_object_class_override_property (klass, PIKA_ACTION_PROP_VIEWABLE, "viewable"); g_object_class_override_property (klass, PIKA_ACTION_PROP_ELLIPSIZE, "ellipsize"); g_object_class_override_property (klass, PIKA_ACTION_PROP_MAX_WIDTH_CHARS, "max-width-chars"); } void pika_action_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PikaActionPrivate *priv; priv = GET_PRIVATE (object); switch (property_id) { case PIKA_ACTION_PROP_CONTEXT: g_value_set_object (value, priv->context); break; case PIKA_ACTION_PROP_SENSITIVE: g_value_set_boolean (value, priv->sensitive); break; case PIKA_ACTION_PROP_VISIBLE: g_value_set_boolean (value, priv->visible); break; case PIKA_ACTION_PROP_LABEL: g_value_set_string (value, priv->label); break; case PIKA_ACTION_PROP_SHORT_LABEL: g_value_set_string (value, priv->short_label ? priv->short_label : priv->label); break; case PIKA_ACTION_PROP_TOOLTIP: g_value_set_string (value, priv->tooltip); break; case PIKA_ACTION_PROP_ICON_NAME: g_value_set_string (value, priv->icon_name); break; case PIKA_ACTION_PROP_ICON: g_value_set_object (value, priv->icon); break; case PIKA_ACTION_PROP_COLOR: g_value_set_boxed (value, priv->color); break; case PIKA_ACTION_PROP_VIEWABLE: g_value_set_object (value, priv->viewable); break; case PIKA_ACTION_PROP_ELLIPSIZE: g_value_set_enum (value, priv->ellipsize); break; case PIKA_ACTION_PROP_MAX_WIDTH_CHARS: g_value_set_int (value, priv->max_width_chars); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } void pika_action_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaActionPrivate *priv; gboolean set_proxy = FALSE; priv = GET_PRIVATE (object); switch (property_id) { case PIKA_ACTION_PROP_CONTEXT: g_set_object (&priv->context, g_value_get_object (value)); break; case PIKA_ACTION_PROP_SENSITIVE: pika_action_set_sensitive (PIKA_ACTION (object), g_value_get_boolean (value), NULL); break; case PIKA_ACTION_PROP_VISIBLE: if (priv->visible != g_value_get_boolean (value)) { priv->visible = g_value_get_boolean (value); /* Only notify when the state actually changed. This is important for * handlers such as visibility of menu items in PikaMenuModel which * will assume that the action visibility changed. Otherwise we might * remove items by mistake. */ g_object_notify (object, "visible"); } break; case PIKA_ACTION_PROP_LABEL: pika_action_set_label (PIKA_ACTION (object), g_value_get_string (value)); break; case PIKA_ACTION_PROP_SHORT_LABEL: pika_action_set_short_label (PIKA_ACTION (object), g_value_get_string (value)); break; case PIKA_ACTION_PROP_TOOLTIP: pika_action_set_tooltip (PIKA_ACTION (object), g_value_get_string (value)); break; case PIKA_ACTION_PROP_ICON_NAME: pika_action_set_icon_name (PIKA_ACTION (object), g_value_get_string (value)); break; case PIKA_ACTION_PROP_ICON: pika_action_set_gicon (PIKA_ACTION (object), g_value_get_object (value)); break; case PIKA_ACTION_PROP_COLOR: g_clear_pointer (&priv->color, g_free); priv->color = g_value_dup_boxed (value); set_proxy = TRUE; break; case PIKA_ACTION_PROP_VIEWABLE: g_set_object (&priv->viewable, g_value_get_object (value)); set_proxy = TRUE; break; case PIKA_ACTION_PROP_ELLIPSIZE: priv->ellipsize = g_value_get_enum (value); set_proxy = TRUE; break; case PIKA_ACTION_PROP_MAX_WIDTH_CHARS: priv->max_width_chars = g_value_get_int (value); set_proxy = TRUE; break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } if (set_proxy) { /* Set or update the proxy rendering. */ for (GList *list = priv->proxies; list; list = list->next) pika_action_set_proxy (PIKA_ACTION (object), list->data); } } void pika_action_set_proxy (PikaAction *action, GtkWidget *proxy) { PikaActionPrivate *priv = GET_PRIVATE (action); GtkWidget *proxy_image = NULL; GdkPixbuf *pixbuf = NULL; GtkWidget *label; if (! GTK_IS_MENU_ITEM (proxy) && ! GTK_IS_TOOL_BUTTON (proxy) && ! GTK_IS_BUTTON (proxy)) return; /* Current implementation accepts GtkButton as proxies but don't modify their * render. TODO? */ if (! GTK_IS_BUTTON (proxy)) { if (priv->color) { if (GTK_IS_MENU_ITEM (proxy)) proxy_image = pika_menu_item_get_image (GTK_MENU_ITEM (proxy)); else proxy_image = gtk_tool_button_get_label_widget (GTK_TOOL_BUTTON (proxy)); if (PIKA_IS_COLOR_AREA (proxy_image)) { pika_color_area_set_color (PIKA_COLOR_AREA (proxy_image), priv->color); proxy_image = NULL; } else { gint width, height; proxy_image = pika_color_area_new (priv->color, PIKA_COLOR_AREA_SMALL_CHECKS, 0); pika_color_area_set_draw_border (PIKA_COLOR_AREA (proxy_image), TRUE); if (priv->context) pika_color_area_set_color_config (PIKA_COLOR_AREA (proxy_image), priv->context->pika->config->color_management); gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, &height); gtk_widget_set_size_request (proxy_image, width, height); gtk_widget_show (proxy_image); } } else if (priv->viewable) { if (GTK_IS_MENU_ITEM (proxy)) proxy_image = pika_menu_item_get_image (GTK_MENU_ITEM (proxy)); else proxy_image = gtk_tool_button_get_label_widget (GTK_TOOL_BUTTON (proxy)); if (PIKA_IS_VIEW (proxy_image) && g_type_is_a (G_TYPE_FROM_INSTANCE (priv->viewable), PIKA_VIEW (proxy_image)->renderer->viewable_type)) { pika_view_set_viewable (PIKA_VIEW (proxy_image), priv->viewable); proxy_image = NULL; } else { GtkIconSize size; gint width, height; gint border_width; if (PIKA_IS_IMAGEFILE (priv->viewable)) { size = GTK_ICON_SIZE_LARGE_TOOLBAR; border_width = 0; } else { size = GTK_ICON_SIZE_MENU; border_width = 1; } gtk_icon_size_lookup (size, &width, &height); proxy_image = pika_view_new_full (priv->context, priv->viewable, width, height, border_width, FALSE, FALSE, FALSE); gtk_widget_show (proxy_image); } } else { if (PIKA_IS_PROCEDURE_ACTION (action) && /* Some special cases PikaProcedureAction have no procedure attached * (e.g. "filters-recent-*" actions. */ G_IS_OBJECT (PIKA_PROCEDURE_ACTION (action)->procedure)) { /* Special-casing procedure actions as plug-ins can create icons with * pika_procedure_set_icon_pixbuf(). */ g_object_get (PIKA_PROCEDURE_ACTION (action)->procedure, "icon-pixbuf", &pixbuf, NULL); if (pixbuf != NULL) { gint width; gint height; gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, &height); if (width != gdk_pixbuf_get_width (pixbuf) || height != gdk_pixbuf_get_height (pixbuf)) { GdkPixbuf *copy; copy = gdk_pixbuf_scale_simple (pixbuf, width, height, GDK_INTERP_BILINEAR); g_object_unref (pixbuf); pixbuf = copy; } proxy_image = gtk_image_new_from_pixbuf (pixbuf); } } if (proxy_image == NULL) { if (GTK_IS_MENU_ITEM (proxy)) proxy_image = pika_menu_item_get_image (GTK_MENU_ITEM (proxy)); else proxy_image = gtk_tool_button_get_label_widget (GTK_TOOL_BUTTON (proxy)); if (PIKA_IS_VIEW (proxy_image) || PIKA_IS_COLOR_AREA (proxy_image)) { if (GTK_IS_MENU_ITEM (proxy)) pika_menu_item_set_image (GTK_MENU_ITEM (proxy), NULL, action); else if (proxy_image) gtk_widget_destroy (proxy_image); g_object_notify (G_OBJECT (action), "icon-name"); } proxy_image = NULL; } } } if (proxy_image != NULL) { if (GTK_IS_MENU_ITEM (proxy)) { pika_menu_item_set_image (GTK_MENU_ITEM (proxy), proxy_image, action); } else /* GTK_IS_TOOL_BUTTON (proxy) */ { GtkWidget *prev_widget; prev_widget = gtk_tool_button_get_label_widget (GTK_TOOL_BUTTON (proxy)); if (prev_widget) gtk_widget_destroy (prev_widget); gtk_tool_button_set_label_widget (GTK_TOOL_BUTTON (proxy), proxy_image); } } else if (GTK_IS_LABEL (gtk_bin_get_child (GTK_BIN (proxy))) && GTK_IS_MENU_ITEM (proxy)) { /* Ensure we rebuild the contents of the GtkMenuItem with an image (which * might be NULL), a label (taken from action) and an optional shortcut * (also taken from action). */ pika_menu_item_set_image (GTK_MENU_ITEM (proxy), NULL, action); } label = g_object_get_data (G_OBJECT (proxy), "pika-menu-item-label"); if (label) { gtk_label_set_ellipsize (GTK_LABEL (label), priv->ellipsize); gtk_label_set_max_width_chars (GTK_LABEL (label), priv->max_width_chars); } if (! g_list_find (priv->proxies, proxy)) { priv->proxies = g_list_prepend (priv->proxies, proxy); g_signal_connect (proxy, "destroy", (GCallback) pika_action_proxy_destroy, action); if (GTK_IS_BUTTON (proxy)) g_signal_connect (proxy, "clicked", (GCallback) pika_action_proxy_button_activate, action); pika_action_update_proxy_sensitive (action, proxy); } g_clear_object (&pixbuf); } /* Friend functions */ /* This function is only meant to be run by the PikaActionGroup class. */ void pika_action_set_group (PikaAction *action, PikaActionGroup *group) { PikaActionPrivate *priv = GET_PRIVATE (action); /* We can't change groups! */ g_return_if_fail (priv->group == NULL); priv->group = group; } /* This function is only meant to be run by the PikaActionGroup class. */ void pika_action_set_default_accels (PikaAction *action, const gchar **accels) { PikaActionPrivate *priv = GET_PRIVATE (action); g_return_if_fail (PIKA_IS_ACTION (action)); /* This should be set only once and before any accelerator was set. */ g_return_if_fail (priv->accels == NULL); g_return_if_fail (priv->default_accels == NULL); priv->default_accels = g_strdupv ((gchar **) accels); priv->accels = g_strdupv ((gchar **) accels); g_signal_emit (action, action_signals[ACCELS_CHANGED], 0, accels); } /* This function is only meant to be run by the PikaMenuModel class. */ void pika_action_set_menu_path (PikaAction *action, const gchar *menu_path) { PikaActionPrivate *priv; gchar **paths; g_return_if_fail (PIKA_IS_ACTION (action)); priv = GET_PRIVATE (action); if (priv->menu_path != NULL) /* There are cases where we put some actions in 2 menu paths, for instance: * - filters-color-to-alpha in both /Layer/Transparency and /Colors * - dialogs-histogram in both /Colors/Info and /Windows/Dockable Dialogs * * Anyway this is not an error, it's just how it is. Let's simply skip such * cases silently and keep the first path as reference to show in helper * widgets. */ return; if (menu_path) { paths = pika_utils_break_menu_path (menu_path, NULL, NULL); /* The 4 raw bytes are the "rightwards triangle arrowhead" unicode character. */ priv->menu_path = g_strjoinv (" \xF0\x9F\xA2\x92 ", paths); g_strfreev (paths); } } /* Private functions */ static PikaActionPrivate * pika_action_get_private (PikaAction *action) { PikaActionPrivate *priv; static GQuark private_key = 0; g_return_val_if_fail (PIKA_IS_ACTION (action), NULL); if (! private_key) private_key = g_quark_from_static_string ("pika-action-priv"); priv = g_object_get_qdata ((GObject *) action, private_key); if (! priv) { priv = g_slice_new0 (PikaActionPrivate); g_object_set_qdata_full ((GObject *) action, private_key, priv, (GDestroyNotify) pika_action_private_finalize); } return priv; } static void pika_action_private_finalize (PikaActionPrivate *priv) { g_clear_pointer (&priv->disable_reason, g_free); g_clear_object (&priv->context); g_clear_pointer (&priv->color, g_free); g_clear_object (&priv->viewable); g_free (priv->label); g_free (priv->short_label); g_free (priv->tooltip); g_free (priv->icon_name); g_clear_object (&priv->icon); g_strfreev (priv->accels); g_strfreev (priv->default_accels); g_free (priv->menu_path); for (GList *iter = priv->proxies; iter; iter = iter->next) { /* TODO GAction: if an action associated to a proxy menu item disappears, * shouldn't we also destroy the item itself (not just disconnect it)? It * would now point to a non-existing action. */ g_signal_handlers_disconnect_by_func (iter->data, pika_action_proxy_destroy, priv->action); g_signal_handlers_disconnect_by_func (iter->data, pika_action_proxy_button_activate, priv->action); } g_list_free (priv->proxies); priv->proxies = NULL; g_slice_free (PikaActionPrivate, priv); } static void pika_action_set_proxy_label (PikaAction *action, GtkWidget *proxy) { if (GTK_IS_MENU_ITEM (proxy)) { GtkWidget *child = gtk_bin_get_child (GTK_BIN (proxy)); const gchar *label; /* For menus, we assume that their position ensure some context, hence the * short label is chosen in priority. */ label = pika_action_get_short_label (action); if (GTK_IS_BOX (child)) { child = g_object_get_data (G_OBJECT (proxy), "pika-menu-item-label"); if (GTK_IS_LABEL (child)) gtk_label_set_text_with_mnemonic (GTK_LABEL (child), label); } else if (GTK_IS_LABEL (child)) { gtk_menu_item_set_label (GTK_MENU_ITEM (proxy), label); } } } static void pika_action_label_notify (PikaAction *action, const GParamSpec *pspec, gpointer data) { for (GList *iter = GET_PRIVATE (action)->proxies; iter; iter = iter->next) pika_action_set_proxy_label (action, iter->data); } static void pika_action_proxy_destroy (GtkWidget *proxy, PikaAction *action) { PikaActionPrivate *priv = GET_PRIVATE (action); priv->proxies = g_list_remove (priv->proxies, proxy); } static void pika_action_proxy_button_activate (GtkButton *button, PikaAction *action) { /* Activate with the default parameter value. */ pika_action_activate (action); } static void pika_action_update_proxy_sensitive (PikaAction *action, GtkWidget *proxy) { PikaActionPrivate *priv = GET_PRIVATE (action); gboolean sensitive = pika_action_is_sensitive (action, NULL); if (proxy) { gtk_widget_set_sensitive (proxy, sensitive); pika_action_update_proxy_tooltip (action, proxy); } else { for (GList *list = priv->proxies; list; list = list->next) gtk_widget_set_sensitive (list->data, sensitive); pika_action_update_proxy_tooltip (action, NULL); } } static void pika_action_update_proxy_tooltip (PikaAction *action, GtkWidget *proxy) { PikaActionPrivate *priv; const gchar *tooltip; const gchar *help_id; const gchar *reason = NULL; gchar *escaped_reason = NULL; gchar *markup; g_return_if_fail (PIKA_IS_ACTION (action)); priv = GET_PRIVATE (action); tooltip = pika_action_get_tooltip (action); help_id = pika_action_get_help_id (action); pika_action_get_sensitive (action, &reason); if (reason) escaped_reason = g_markup_escape_text (reason, -1); markup = g_strdup_printf ("%s%s" /* Action tooltip */ "%s", /* Inactive reason */ tooltip, escaped_reason && tooltip ? "\n" : "", escaped_reason ? escaped_reason : ""); /* This hack makes sure we don't replace the tooltips of PikaButtons * with extended callbacks (for Shift+Click etc.), because these * buttons already have customly constructed multi-line tooltips * which we want to keep. */ #define HAS_EXTENDED_ACTIONS(widget) \ (g_object_get_data (G_OBJECT (widget), "extended-actions") != NULL) if (proxy != NULL) { if (! HAS_EXTENDED_ACTIONS (proxy)) pika_help_set_help_data_with_markup (proxy, markup, help_id); } else { for (GList *list = priv->proxies; list; list = list->next) if (! HAS_EXTENDED_ACTIONS (list->data)) pika_help_set_help_data_with_markup (list->data, markup, help_id); } g_free (escaped_reason); g_free (markup); }