1
1
mirror of https://gitlab.gnome.org/GNOME/gimp.git synced 2025-10-06 01:12:40 +02:00
Files
gimp/app/widgets/gimpmenushell.c
Jehan b1f022f144 app, menus: now support the child menu of the GimpDockbook menu.
I only translated the undo menu into GtkBuilder's .ui format for now.
The only missing part is that the icon is now shown.

Note that in various parts, I don't rely anymore on a bogus menu action (i.e.
"undo-popup" action in this case) which does nothing but has an associated label
and icon. I simply add the label and icon as submenu attribute directly in the
.ui file, which is translatable and whose strings should be parsed by gettext.

Eventually I'll just get rid of all the various "*-popup" or "*-menu" bogus
actions.
2023-04-12 22:07:08 +02:00

805 lines
25 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* gimpmenushell.c
* Copyright (C) 2023 Jehan
*
* 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 "libgimpbase/gimpbase.h"
#include "libgimpwidgets/gimpwidgets.h"
#include "widgets-types.h"
#include "core/gimp.h"
#include "gimpaction.h"
#include "gimpenumaction.h"
#include "gimphelp-ids.h"
#include "gimpmenu.h"
#include "gimpmenushell.h"
#include "gimpprocedureaction.h"
#include "gimpradioaction.h"
#include "gimptoggleaction.h"
#include "gimpuimanager.h"
#define GET_PRIVATE(obj) (gimp_menu_shell_get_private ((GimpMenuShell *) (obj)))
typedef struct _GimpMenuShellPrivate GimpMenuShellPrivate;
struct _GimpMenuShellPrivate
{
GimpUIManager *manager;
gchar *update_signal;
GTree *submenus;
GTree *placeholders;
GRegex *path_regex;
};
static GimpMenuShellPrivate *
gimp_menu_shell_get_private (GimpMenuShell *menu_shell);
static void gimp_menu_shell_private_finalize (GimpMenuShellPrivate *priv);
static void gimp_menu_shell_help_fun (const gchar *bogus_help_id,
gpointer help_data);
static void gimp_menu_shell_ui_added (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top,
GimpMenuShell *shell);
static void gimp_menu_shell_radio_item_toggled (GtkWidget *item,
GAction *action);
static void gimp_menu_shell_action_activate (GtkMenuItem *item,
GimpAction *action);
static void gimp_menu_shell_toggle_action_changed (GimpAction *action,
GVariant *value G_GNUC_UNUSED,
GtkCheckMenuItem *item);
static void gimp_menu_shell_action_notify_sensitive (GimpAction *action,
const GParamSpec *pspec,
GtkCheckMenuItem *item);
static void gimp_menu_shell_action_notify_visible (GimpAction *action,
const GParamSpec *pspec,
GtkWidget *item);
static void gimp_menu_shell_append_model (GimpMenuShell *shell,
GMenuModel *model);
static void gimp_menu_shell_add_ui (GimpMenuShell *shell,
const gchar **paths,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top);
static void gimp_menu_shell_add_action (GimpMenuShell *shell,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top,
GtkRadioMenuItem **group);
static void gimp_menu_shell_add_placeholder (GimpMenuShell *shell,
const gchar *label);
static gchar ** gimp_menu_shell_break_path (GimpMenuShell *shell,
const gchar *path);
static gchar * gimp_menu_shell_make_canonical_path (const gchar *path);
G_DEFINE_INTERFACE (GimpMenuShell, gimp_menu_shell, GTK_TYPE_MENU_SHELL)
static void
gimp_menu_shell_default_init (GimpMenuShellInterface *iface)
{
g_object_interface_install_property (iface,
g_param_spec_object ("manager",
NULL, NULL,
GIMP_TYPE_UI_MANAGER,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT_ONLY));
}
/* public functions */
void
gimp_menu_shell_fill (GimpMenuShell *shell,
GMenuModel *model,
const gchar *update_signal)
{
g_return_if_fail (GTK_IS_CONTAINER (shell));
gtk_container_foreach (GTK_CONTAINER (shell),
(GtkCallback) gtk_widget_destroy,
NULL);
gimp_menu_shell_append_model (shell, model);
if (update_signal != NULL)
{
GimpMenuShellPrivate *priv = GET_PRIVATE (shell);
if (priv->update_signal != NULL)
{
g_free (priv->update_signal);
g_signal_handlers_disconnect_by_func (priv->manager,
G_CALLBACK (gimp_menu_shell_ui_added),
shell);
}
priv->update_signal = g_strdup (update_signal);
g_signal_connect (priv->manager, update_signal,
G_CALLBACK (gimp_menu_shell_ui_added),
shell);
gimp_ui_manager_foreach_ui (priv->manager,
(GimpUIMenuCallback) gimp_menu_shell_ui_added,
shell);
}
}
void
gimp_menu_shell_merge (GimpMenuShell *shell,
GimpMenuShell *shell2,
gboolean top)
{
GList *children;
GList *iter;
children = gtk_container_get_children (GTK_CONTAINER (shell2));
iter = top ? g_list_last (children) : children;
for (; iter; iter = top ? iter->prev : iter->next)
{
GtkWidget *item = iter->data;
g_object_ref (item);
gtk_container_remove (GTK_CONTAINER (shell2), item);
if (top)
gtk_menu_shell_prepend (GTK_MENU_SHELL (shell), item);
else
gtk_menu_shell_append (GTK_MENU_SHELL (shell), item);
g_object_unref (item);
}
g_list_free (children);
gtk_widget_destroy (GTK_WIDGET (shell2));
}
/* Protected functions. */
void
gimp_menu_shell_init (GimpMenuShell *shell)
{
GimpMenuShellPrivate *priv;
g_return_if_fail (GIMP_IS_MENU_SHELL (shell));
priv = GET_PRIVATE (shell);
priv->submenus = g_tree_new_full ((GCompareDataFunc) g_strcmp0, NULL,
g_free, NULL);
priv->placeholders = g_tree_new_full ((GCompareDataFunc) g_strcmp0, NULL,
g_free, NULL);
priv->update_signal = NULL;
priv->path_regex = g_regex_new ("/+", 0, 0, NULL);
gimp_help_connect (GTK_WIDGET (shell),
gimp_menu_shell_help_fun,
GIMP_HELP_MAIN,
shell,
NULL);
}
/**
* gimp_menu_shell_install_properties:
* @klass: the class structure for a type deriving from #GObject
*
* Installs the necessary properties for a class implementing
* #GimpMenuShell. Please call this function in the *_class_init()
* function of the child class.
**/
void
gimp_menu_shell_install_properties (GObjectClass *klass)
{
g_object_class_override_property (klass, GIMP_MENU_SHELL_PROP_MANAGER, "manager");
}
void
gimp_menu_shell_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpMenuShellPrivate *priv;
priv = GET_PRIVATE (object);
switch (property_id)
{
case GIMP_MENU_SHELL_PROP_MANAGER:
g_value_set_object (value, priv->manager);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
void
gimp_menu_shell_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpMenuShellPrivate *priv;
priv = GET_PRIVATE (object);
switch (property_id)
{
case GIMP_MENU_SHELL_PROP_MANAGER:
g_set_weak_pointer (&priv->manager, g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
/* Private functions */
static GimpMenuShellPrivate *
gimp_menu_shell_get_private (GimpMenuShell *menu_shell)
{
GimpMenuShellPrivate *priv;
static GQuark private_key = 0;
g_return_val_if_fail (GIMP_IS_MENU_SHELL (menu_shell), NULL);
if (! private_key)
private_key = g_quark_from_static_string ("gimp-menu_shell-priv");
priv = g_object_get_qdata ((GObject *) menu_shell, private_key);
if (! priv)
{
priv = g_slice_new0 (GimpMenuShellPrivate);
g_object_set_qdata_full ((GObject *) menu_shell, private_key, priv,
(GDestroyNotify) gimp_menu_shell_private_finalize);
}
return priv;
}
static void
gimp_menu_shell_private_finalize (GimpMenuShellPrivate *priv)
{
g_tree_unref (priv->submenus);
g_tree_unref (priv->placeholders);
g_free (priv->update_signal);
g_clear_pointer (&priv->path_regex, g_regex_unref);
g_slice_free (GimpMenuShellPrivate, priv);
}
static void
gimp_menu_shell_help_fun (const gchar *bogus_help_id,
gpointer help_data)
{
GimpMenuShellPrivate *priv;
gchar *help_id = NULL;
GimpMenuShell *shell;
Gimp *gimp;
GtkWidget *item;
gchar *help_domain = NULL;
gchar *help_string = NULL;
gchar *domain_separator;
g_return_if_fail (GIMP_IS_MENU_SHELL (help_data));
shell = GIMP_MENU_SHELL (help_data);
priv = GET_PRIVATE (shell);
gimp = priv->manager->gimp;
g_return_if_fail (GIMP_IS_GIMP (gimp));
item = gtk_menu_shell_get_selected_item (GTK_MENU_SHELL (shell));
if (item)
help_id = g_object_get_qdata (G_OBJECT (item), GIMP_HELP_ID);
if (help_id == NULL || strlen (help_id) == 0)
help_id = (gchar *) bogus_help_id;
help_id = g_strdup (help_id);
domain_separator = strchr (help_id, '?');
if (domain_separator)
{
*domain_separator = '\0';
help_domain = g_strdup (help_id);
help_string = g_strdup (domain_separator + 1);
}
else
{
help_string = g_strdup (help_id);
}
gimp_help (gimp, NULL, help_domain, help_string);
g_free (help_domain);
g_free (help_string);
g_free (help_id);
}
static void
gimp_menu_shell_ui_added (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top,
GimpMenuShell *shell)
{
gchar **paths = gimp_menu_shell_break_path (shell, path);
gimp_menu_shell_add_ui (shell, (const gchar **) paths,
action_name, placeholder_key, top);
g_strfreev (paths);
}
static void
gimp_menu_shell_radio_item_toggled (GtkWidget *item,
GAction *action)
{
gboolean active = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item));
gimp_toggle_action_set_active (GIMP_TOGGLE_ACTION (action), active);
}
static void
gimp_menu_shell_action_activate (GtkMenuItem *item,
GimpAction *action)
{
gimp_action_activate (action);
}
static void
gimp_menu_shell_toggle_action_changed (GimpAction *action,
/* Unused because this is used for 2 signals
* where the GVariant refers to different data.
*/
GVariant *value G_GNUC_UNUSED,
GtkCheckMenuItem *item)
{
gchar *action_name;
gboolean active;
action_name = g_strdup (gtk_actionable_get_action_name (GTK_ACTIONABLE (item)));
active = gimp_toggle_action_get_active (GIMP_TOGGLE_ACTION (action));
/* Make sure we don't activate the action. */
gtk_actionable_set_action_name (GTK_ACTIONABLE (item), NULL);
gtk_check_menu_item_set_active (item, active);
gtk_actionable_set_action_name (GTK_ACTIONABLE (item), action_name);
g_free (action_name);
}
static void
gimp_menu_shell_action_notify_sensitive (GimpAction *action,
const GParamSpec *pspec,
GtkCheckMenuItem *item)
{
gtk_widget_set_sensitive (GTK_WIDGET (item),
gimp_action_is_sensitive (action, NULL));
}
static void
gimp_menu_shell_action_notify_visible (GimpAction *action,
const GParamSpec *pspec,
GtkWidget *item)
{
GtkWidget *container;
gtk_widget_set_visible (item, gimp_action_is_visible (action));
container = gtk_widget_get_parent (item);
if (gimp_action_is_visible (GIMP_ACTION (action)))
{
GtkWidget *widget = gtk_menu_get_attach_widget (GTK_MENU (container));
/* We must show the GtkMenuItem associated as submenu to the parent
* container.
*/
if (G_TYPE_FROM_INSTANCE (widget) == GTK_TYPE_MENU_ITEM)
gtk_widget_show (widget);
}
else
{
GList *children = gtk_container_get_children (GTK_CONTAINER (container));
gboolean all_invisible = TRUE;
for (GList *iter = children; iter; iter = iter->next)
{
if (gtk_widget_get_visible (iter->data))
{
all_invisible = FALSE;
break;
}
}
g_list_free (children);
if (all_invisible)
{
GtkWidget *widget;
/* No need to leave empty submenus. */
widget = gtk_menu_get_attach_widget (GTK_MENU (container));
if (G_TYPE_FROM_INSTANCE (widget) == GTK_TYPE_MENU_ITEM)
gtk_widget_hide (widget);
}
}
}
static void
gimp_menu_shell_append_model (GimpMenuShell *shell,
GMenuModel *model)
{
GimpMenuShellPrivate *priv;
static GtkRadioMenuItem *group = NULL;
gint n_items;
g_return_if_fail (GTK_IS_CONTAINER (shell));
priv = GET_PRIVATE (shell);
n_items = g_menu_model_get_n_items (model);
for (gint i = 0; i < n_items; i++)
{
GMenuModel *subsection;
GMenuModel *submenu;
GtkWidget *item;
gchar *label = NULL;
gchar *action_name = NULL;
subsection = g_menu_model_get_item_link (model, i, G_MENU_LINK_SECTION);
submenu = g_menu_model_get_item_link (model, i, G_MENU_LINK_SUBMENU);
g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_ACTION, "s", &action_name);
if (subsection != NULL)
{
group = NULL;
item = gtk_separator_menu_item_new ();
gtk_container_add (GTK_CONTAINER (shell), item);
gtk_widget_show (item);
gimp_menu_shell_append_model (shell, subsection);
item = gtk_separator_menu_item_new ();
gtk_container_add (GTK_CONTAINER (shell), item);
gtk_widget_show (item);
}
else if (submenu != NULL)
{
GtkWidget *subcontainer;
GtkWidget *item;
group = NULL;
/* I don't show the item on purpose because
* gimp_menu_shell_append_model() will show the parent item if any of
* the added actions are visible.
*/
item = gtk_menu_item_new_with_mnemonic (label);
gtk_container_add (GTK_CONTAINER (shell), item);
subcontainer = gimp_menu_new (priv->manager);
gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), subcontainer);
gimp_menu_shell_append_model (GIMP_MENU_SHELL (subcontainer), submenu);
gtk_widget_show (subcontainer);
g_tree_insert (priv->submenus,
gimp_menu_shell_make_canonical_path (label),
subcontainer);
}
else if (action_name == NULL)
{
/* Special case: we use items with no action and a label as
* placeholder which allows us to specify a placement in menus, which
* might not be only top or bottom.
*/
g_return_if_fail (label != NULL);
group = NULL;
gimp_menu_shell_add_placeholder (shell, label);
}
else
{
gimp_menu_shell_add_action (shell, action_name, NULL, FALSE, &group);
}
g_free (label);
g_free (action_name);
}
}
static void
gimp_menu_shell_add_ui (GimpMenuShell *shell,
const gchar **paths,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top)
{
g_return_if_fail (paths != NULL);
if (paths[0] == NULL)
{
gimp_menu_shell_add_action (shell, action_name, placeholder_key, top, NULL);
}
else
{
GimpMenuShellPrivate *priv = GET_PRIVATE (shell);
GtkWidget *submenu = NULL;
submenu = g_tree_lookup (priv->submenus, paths[0]);
if (submenu == NULL)
{
GtkWidget *item;
item = gtk_menu_item_new_with_mnemonic (paths[0]);
gtk_container_add (GTK_CONTAINER (shell), item);
submenu = gimp_menu_new (priv->manager);
gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
gtk_widget_show (submenu);
g_tree_insert (priv->submenus, g_strdup (paths[0]), submenu);
}
gimp_menu_shell_add_ui (GIMP_MENU_SHELL (submenu), paths + 1,
action_name, placeholder_key, top);
}
}
static void
gimp_menu_shell_add_action (GimpMenuShell *shell,
const gchar *action_name,
const gchar *placeholder_key,
gboolean top,
GtkRadioMenuItem **group)
{
GimpMenuShellPrivate *priv;
GApplication *app;
GAction *action;
const gchar *action_label;
GtkWidget *item;
GtkWidget *sibling = NULL;
gboolean visible;
g_return_if_fail (GIMP_IS_MENU_SHELL (shell));
priv = GET_PRIVATE (shell);
app = priv->manager->gimp->app;
if (g_str_has_prefix (action_name, "app."))
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name + 4);
else
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name);
g_return_if_fail (GIMP_IS_ACTION (action));
action_label = gimp_action_get_label (GIMP_ACTION (action));
g_return_if_fail (action_label != NULL);
if (GIMP_IS_RADIO_ACTION (action))
{
item = gtk_radio_menu_item_new_with_mnemonic_from_widget (group ? *group : NULL, action_label);
gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
gimp_toggle_action_get_active (GIMP_TOGGLE_ACTION (action)));
if (group)
*group = GTK_RADIO_MENU_ITEM (item);
g_signal_connect (item, "toggled",
G_CALLBACK (gimp_menu_shell_radio_item_toggled),
action);
g_signal_connect (action, "change-state",
G_CALLBACK (gimp_menu_shell_toggle_action_changed),
item);
}
else if (GIMP_IS_TOGGLE_ACTION (action))
{
item = gtk_check_menu_item_new_with_mnemonic (action_label);
gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item),
gimp_toggle_action_get_active (GIMP_TOGGLE_ACTION (action)));
if (group)
*group = NULL;
g_signal_connect (action, "change-state",
G_CALLBACK (gimp_menu_shell_toggle_action_changed),
item);
}
else if (GIMP_IS_PROCEDURE_ACTION (action) ||
GIMP_IS_ENUM_ACTION (action))
{
item = gtk_menu_item_new_with_mnemonic (action_label);
if (group)
*group = NULL;
g_signal_connect (item, "activate",
G_CALLBACK (gimp_menu_shell_action_activate),
action);
}
else
{
item = gtk_menu_item_new_with_mnemonic (action_label);
if (group)
*group = NULL;
}
gtk_actionable_set_action_name (GTK_ACTIONABLE (item), action_name);
gimp_action_set_proxy (GIMP_ACTION (action), item);
gtk_widget_set_sensitive (GTK_WIDGET (item),
gimp_action_is_sensitive (GIMP_ACTION (action), NULL));
g_signal_connect_object (action, "notify::sensitive",
G_CALLBACK (gimp_menu_shell_action_notify_sensitive),
item, 0);
if (placeholder_key)
{
sibling = g_tree_lookup (priv->placeholders, placeholder_key);
if (! sibling)
g_warning ("%s: no placeholder item '%s'.", G_STRFUNC, placeholder_key);
}
if (sibling)
{
GList *children;
gint position = 0;
/* I am assuming that the order of the children list reflects the
* position, though it is not clearly specified in the function docs. Yet
* I could find no other function giving me the position of some child in
* a container.
*/
children = gtk_container_get_children (GTK_CONTAINER (shell));
for (GList *iter = children; iter; iter = iter->next)
{
if (iter->data == sibling)
break;
position++;
}
if (! top)
position++;
gtk_menu_shell_insert (GTK_MENU_SHELL (shell), item, position);
g_list_free (children);
}
else
{
if (top)
gtk_menu_shell_prepend (GTK_MENU_SHELL (shell), item);
else
gtk_menu_shell_append (GTK_MENU_SHELL (shell), item);
}
visible = gimp_action_is_visible (GIMP_ACTION (action));
gtk_widget_set_visible (item, visible);
if (visible && GTK_IS_MENU (shell))
{
GtkWidget *parent = gtk_menu_get_attach_widget (GTK_MENU (shell));
/* Note that this is not the container we must show, but the menu item
* attached to the parent, in order not to leave empty submenus.
*/
if (parent != NULL &&
G_TYPE_FROM_INSTANCE (parent) == GTK_TYPE_MENU_ITEM)
gtk_widget_show (parent);
}
g_signal_connect_object (action, "notify::visible",
G_CALLBACK (gimp_menu_shell_action_notify_visible),
item, 0);
}
static void
gimp_menu_shell_add_placeholder (GimpMenuShell *shell,
const gchar *label)
{
GimpMenuShellPrivate *priv = GET_PRIVATE (shell);
GtkWidget *item;
/* Placeholders are inserted yet never shown, on purpose. */
item = gtk_menu_item_new_with_mnemonic (label);
gtk_container_add (GTK_CONTAINER (shell), item);
g_tree_insert (priv->placeholders, g_strdup (label), item);
}
static gchar **
gimp_menu_shell_break_path (GimpMenuShell *shell,
const gchar *path)
{
GimpMenuShellPrivate *priv;
gchar **paths;
gint start = 0;
g_return_val_if_fail (path != NULL, NULL);
priv = GET_PRIVATE (shell);
/* Get rid of leading slashes. */
while (path[start] == '/' && path[start] != '\0')
start++;
/* Get rid of empty last item because of trailing slashes. */
paths = g_regex_split (priv->path_regex, path + start, 0);
if (strlen (paths[g_strv_length (paths) - 1]) == 0)
{
g_free (paths[g_strv_length (paths) - 1]);
paths[g_strv_length (paths) - 1] = NULL;
}
for (int i = 0; paths[i]; i++)
{
gchar *canon_path;
canon_path = gimp_menu_shell_make_canonical_path (paths[i]);
g_free (paths[i]);
paths[i] = canon_path;
}
return paths;
}
static gchar *
gimp_menu_shell_make_canonical_path (const gchar *path)
{
gchar **split_path;
gchar *canon_path;
/* The first underscore of each path item is a mnemonic. */
split_path = g_strsplit (path, "_", 2);
canon_path = g_strjoinv ("", split_path);
g_strfreev (split_path);
return canon_path;
}