PIKApp/app/widgets/pikaaction.c

1467 lines
45 KiB
C

/* 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 <mitch@gimp.org>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl.h>
#include <gtk/gtk.h>
#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_visible (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)
{
PikaActionPrivate *priv = GET_PRIVATE (action);
/* 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.
*/
if (priv->visible != visible)
{
priv->visible = visible;
pika_action_update_proxy_visible (action, NULL);
g_object_notify (G_OBJECT (action), "visible");
}
}
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;
}
gboolean
pika_action_is_default_accel (PikaAction *action,
const gchar *accel)
{
gchar **default_accels;
guint accelerator_key = 0;
GdkModifierType accelerator_mods = 0;
g_return_val_if_fail (PIKA_IS_ACTION (action), TRUE);
g_return_val_if_fail (accel != NULL, TRUE);
gtk_accelerator_parse (accel, &accelerator_key, &accelerator_mods);
g_return_val_if_fail (accelerator_key != 0 || accelerator_mods == 0, FALSE);
default_accels = GET_PRIVATE (action)->default_accels;
if (default_accels == NULL)
return FALSE;
for (gint i = 0; default_accels[i] != NULL; i++)
{
guint default_key;
GdkModifierType default_mods;
gtk_accelerator_parse (default_accels[i], &default_key, &default_mods);
if (default_key == accelerator_key && default_mods == accelerator_mods)
return TRUE;
}
return FALSE;
}
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:
pika_action_set_visible (PIKA_ACTION (object),
g_value_get_boolean (value));
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_visible (PikaAction *action,
GtkWidget *proxy)
{
PikaActionPrivate *priv = GET_PRIVATE (action);
gboolean visible = pika_action_is_visible (action);
if (proxy)
{
gtk_widget_set_visible (proxy, visible);
}
else
{
for (GList *list = priv->proxies; list; list = list->next)
gtk_widget_set_visible (list->data, visible);
}
}
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 */
"<i><span weight='light'>%s</span></i>", /* 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);
}