Gimp/app/widgets/gimpmenumodel.c
Jacob Boerema 257ac87e9f app: fix #13288 crash when opening a recently closed dock
When at least 3 recently closed docks were present in the Windows ->
Recently Closed Docks submenu, then clicking the middle one caused
a crash. Clicking the top or bottom one didn't cause a crash, but
the submenu was then removed so the other closed docks were not
visible anymore (until restarting GIMP).

It turns out we were removing the whole recent menu, instead of
picking the specific action that needed removing. So now we change
this to get the action_name and use that in the remove call.

Although this already fixes the crash, I added an extra check to
make sure action is valid, and if not we generate a critical. This
way we will notice something is wrong if this happens in the future,
without causing a crash here.
2025-04-04 19:29:04 +00:00

1345 lines
47 KiB
C

/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* gimpmenu_model.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 "widgets-types.h"
#include "libgimpbase/gimpbase.h"
#include "libgimpcolor/gimpcolor.h"
#include "libgimpwidgets/gimpwidgets.h"
#include "core/gimp.h"
#include "gimpaction.h"
#include "gimpactiongroup.h"
#include "gimpmenumodel.h"
#include "gimpmenushell.h"
#include "gimpradioaction.h"
#include "gimpuimanager.h"
#include "gimpwidgets-utils.h"
/**
* GimpMenuModel:
*
* GimpMenuModel implements GMenuModel. We initialize an object from another
* GMenuModel (usually a GMenu), auto-fill with various data from the
* GimpAction, when they are not in GAction API, e.g. labels, but action
* visibility.
*
* The object will also synchronize automatically with changes from the actions,
* but also GimpUIManager for dynamic contents and will trigger an
* "items-changed" when necessary. This allows for such variant to be used in
* GTK API which has no knowledge of GimpAction or GimpUIManager enhancements.
*/
enum
{
PROP_0,
PROP_MANAGER,
PROP_MODEL,
PROP_PATH,
PROP_IS_SECTION,
PROP_TITLE,
PROP_COLOR,
};
struct _GimpMenuModelPrivate
{
GimpUIManager *manager;
GMenuModel *model;
gchar *path;
gboolean is_section;
/* If this GimpMenuModel represents a submenu for a bigger menu, this object
* will not be NULL.
*/
GMenuItem *submenu_item;
GeglColor *submenu_color;
GList *items;
GHashTable *named_sections;
};
static void gimp_menu_model_finalize (GObject *object);
static void gimp_menu_model_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec);
static void gimp_menu_model_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static GVariant * gimp_menu_model_get_item_attribute_value (GMenuModel *model,
gint item_index,
const gchar *attribute,
const GVariantType *expected_type);
static void gimp_menu_model_get_item_attributes (GMenuModel *model,
gint item_index,
GHashTable **attributes);
static GMenuModel * gimp_menu_model_get_item_link (GMenuModel *model,
gint item_index,
const gchar *link);
static void gimp_menu_model_get_item_links (GMenuModel *model,
gint item_index,
GHashTable **links);
static gint gimp_menu_model_get_n_items (GMenuModel *model);
static gint gimp_menu_model_get_position (GimpMenuModel *model,
const gchar *action_name,
gboolean *visible);;
static gboolean gimp_menu_model_is_mutable (GMenuModel *model);
static void gimp_menu_model_initialize (GimpMenuModel *model,
GMenuModel *gmodel);
static gchar * gimp_menu_model_handles_subpath (GimpMenuModel *model,
const gchar *canonical_path,
const gchar *mnemonic_path);
static GMenuItem * gimp_menu_model_get_item (GimpMenuModel *model,
gint idx);
static GMenuItem * gimp_menu_model_get_menu_item_rec (GimpMenuModel *model,
const gchar *path,
GimpMenuModel **menu,
GMenuItem *item);
static void gimp_menu_model_notify_group_label (GimpRadioAction *action,
const GParamSpec *pspec,
GMenuItem *item);
static void gimp_menu_model_action_notify_visible (GimpAction *action,
GParamSpec *pspec,
GimpMenuModel *model);
static void gimp_menu_model_action_notify_label (GimpAction *action,
GParamSpec *pspec,
GimpMenuModel *model);
static gboolean gimp_menu_model_ui_added (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
gboolean top,
GimpMenuModel *model);
static gboolean gimp_menu_model_ui_removed (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
GimpMenuModel *model);
G_DEFINE_TYPE_WITH_CODE (GimpMenuModel, gimp_menu_model, G_TYPE_MENU_MODEL,
G_ADD_PRIVATE (GimpMenuModel))
#define parent_class gimp_menu_model_parent_class
/* Class functions */
static void
gimp_menu_model_class_init (GimpMenuModelClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass);
object_class->finalize = gimp_menu_model_finalize;
object_class->get_property = gimp_menu_model_get_property;
object_class->set_property = gimp_menu_model_set_property;
model_class->get_item_attribute_value = gimp_menu_model_get_item_attribute_value;
model_class->get_item_attributes = gimp_menu_model_get_item_attributes;
model_class->get_item_link = gimp_menu_model_get_item_link;
model_class->get_item_links = gimp_menu_model_get_item_links;
model_class->get_n_items = gimp_menu_model_get_n_items;
model_class->is_mutable = gimp_menu_model_is_mutable;
g_object_class_install_property (object_class, PROP_MANAGER,
g_param_spec_object ("manager",
NULL, NULL,
GIMP_TYPE_UI_MANAGER,
GIMP_PARAM_READWRITE |
G_PARAM_CONSTRUCT));
g_object_class_install_property (object_class, PROP_MODEL,
g_param_spec_object ("model",
NULL, NULL,
G_TYPE_MENU_MODEL,
GIMP_PARAM_READWRITE));
g_object_class_install_property (object_class, PROP_PATH,
g_param_spec_string ("path",
NULL, NULL, NULL,
GIMP_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class, PROP_IS_SECTION,
g_param_spec_boolean ("section",
NULL, NULL, FALSE,
GIMP_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
/* Titles are only relevant if the model is that of a submenu. */
g_object_class_install_property (object_class, PROP_TITLE,
g_param_spec_string ("title",
NULL, NULL, NULL,
GIMP_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY));
g_object_class_install_property (object_class, PROP_COLOR,
gimp_param_spec_color ("color",
NULL, NULL,
TRUE, NULL,
GIMP_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY));
}
static void
gimp_menu_model_init (GimpMenuModel *model)
{
model->priv = gimp_menu_model_get_instance_private (model);
model->priv->items = NULL;
model->priv->path = NULL;
model->priv->is_section = FALSE;
model->priv->submenu_item = NULL;
model->priv->submenu_color = NULL;
model->priv->named_sections = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
}
static void
gimp_menu_model_finalize (GObject *object)
{
GimpMenuModel *model = GIMP_MENU_MODEL (object);
g_clear_weak_pointer (&model->priv->manager);
g_clear_object (&model->priv->model);
g_list_free_full (model->priv->items, g_object_unref);
g_free (model->priv->path);
g_clear_object (&model->priv->submenu_color);
g_hash_table_destroy (model->priv->named_sections);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
void
gimp_menu_model_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
GimpMenuModel *model = GIMP_MENU_MODEL (object);
switch (property_id)
{
case PROP_MANAGER:
g_value_set_object (value, model->priv->manager);
break;
case PROP_MODEL:
g_value_set_object (value, model->priv->model);
break;
case PROP_TITLE:
{
gchar *title;
g_menu_item_get_attribute (model->priv->submenu_item, G_MENU_ATTRIBUTE_LABEL, "s", &title);
g_value_set_string (value, title);
g_free (title);
}
break;
case PROP_COLOR:
g_value_set_object (value, model->priv->submenu_color);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
void
gimp_menu_model_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpMenuModel *model = GIMP_MENU_MODEL (object);
switch (property_id)
{
case PROP_MANAGER:
g_set_weak_pointer (&model->priv->manager, g_value_get_object (value));
break;
case PROP_MODEL:
model->priv->model = g_value_dup_object (value);
gimp_menu_model_initialize (model, model->priv->model);
break;
case PROP_PATH:
model->priv->path = g_value_dup_string (value);
break;
case PROP_IS_SECTION:
model->priv->is_section = g_value_get_boolean (value);
break;
case PROP_TITLE:
gimp_menu_model_set_title (model, model->priv->path,
g_value_get_string (value));
break;
case PROP_COLOR:
gimp_menu_model_set_color (model, model->priv->path,
g_value_get_object (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static GVariant*
gimp_menu_model_get_item_attribute_value (GMenuModel *model,
gint item_index,
const gchar *attribute,
const GVariantType *expected_type)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
GMenuItem *item;
item = gimp_menu_model_get_item (m, item_index);
return g_menu_item_get_attribute_value (item, attribute, expected_type);
}
static void
gimp_menu_model_get_item_attributes (GMenuModel *model,
gint item_index,
GHashTable **attributes)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
GMenuItem *item;
GVariant *value;
item = gimp_menu_model_get_item (m, item_index);
*attributes = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
(GDestroyNotify) g_variant_unref);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_LABEL, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_LABEL, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_ACTION, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_ACTION, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_ICON, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_ICON, value);
value = g_menu_item_get_attribute_value (item, G_MENU_LINK_SUBMENU, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_LINK_SUBMENU, value);
value = g_menu_item_get_attribute_value (item, G_MENU_LINK_SECTION, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_LINK_SECTION, value);
value = g_menu_item_get_attribute_value (item, G_MENU_ATTRIBUTE_TARGET, NULL);
if (value)
g_hash_table_insert (*attributes, G_MENU_ATTRIBUTE_TARGET, value);
value = g_menu_item_get_attribute_value (item, "hidden-when", NULL);
if (value)
g_hash_table_insert (*attributes, "hidden-when", value);
}
static GMenuModel*
gimp_menu_model_get_item_link (GMenuModel *model,
gint item_index,
const gchar *link)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
GMenuItem *item;
item = gimp_menu_model_get_item (m, item_index);
return g_menu_item_get_link (item, link);
}
static void
gimp_menu_model_get_item_links (GMenuModel *model,
gint item_index,
GHashTable **links)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
GMenuModel *subsection;
GMenuModel *submenu;
GMenuItem *item;
*links = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
(GDestroyNotify) g_object_unref);
item = gimp_menu_model_get_item (m, item_index);
subsection = g_menu_item_get_link (item, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (item, G_MENU_LINK_SUBMENU);
if (subsection)
g_hash_table_insert (*links, G_MENU_LINK_SECTION, g_object_ref (subsection));
if (submenu)
g_hash_table_insert (*links, G_MENU_LINK_SUBMENU, g_object_ref (submenu));
g_clear_object (&subsection);
g_clear_object (&submenu);
}
static gint
gimp_menu_model_get_n_items (GMenuModel *model)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
return gimp_menu_model_get_position (m, NULL, NULL);
}
/* This function has 2 usage:
* - Either you call it with @action_name == NULL, then it returns the
* total number of visible items in this model.
* - Or it returns the position of @action_name and its visible state.
*/
static gint
gimp_menu_model_get_position (GimpMenuModel *model,
const gchar *action_name,
gboolean *visible)
{
GList *iter;
gint len = 0;
for (iter = model->priv->items; iter; iter = iter->next)
{
GMenuModel *subsection;
GMenuModel *submenu;
const gchar *cur_action_name = NULL;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (iter->data, G_MENU_LINK_SUBMENU);
if (subsection || submenu)
{
len++;
}
/* Count neither placeholders (items with no action name), nor invisible
* actions.
*/
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &cur_action_name))
{
GimpAction *cur_action = NULL;
const gchar *real_action_name = NULL;
if (cur_action_name != NULL)
{
real_action_name = strstr (cur_action_name, ".");
if (real_action_name != NULL)
real_action_name++;
else
real_action_name = cur_action_name;
cur_action = gimp_ui_manager_find_action (model->priv->manager,
NULL, cur_action_name);
}
if (cur_action_name != NULL &&
g_strcmp0 (action_name, real_action_name) == 0)
{
if (visible)
{
if (cur_action != NULL)
*visible = gimp_action_is_visible (cur_action);
else
/* This may happen when editing a menu item for an action
* which got removed.
*/
*visible = FALSE;
}
break;
}
else if (cur_action != NULL && gimp_action_is_visible (cur_action))
{
len++;
}
}
g_clear_object (&subsection);
g_clear_object (&submenu);
}
g_return_val_if_fail (action_name == NULL || iter != NULL, -1);
return len;
}
static gboolean
gimp_menu_model_is_mutable (GMenuModel* model)
{
return TRUE;
}
/* Public functions. */
GimpMenuModel *
gimp_menu_model_new (GimpUIManager *manager,
GMenuModel *model)
{
g_return_val_if_fail (GIMP_IS_UI_MANAGER (manager), NULL);
return g_object_new (GIMP_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
NULL);
}
GimpMenuModel *
gimp_menu_model_get_submodel (GimpMenuModel *model,
const gchar *path)
{
GimpMenuModel *submodel;
gchar *submenus;
gchar *submenu;
gchar *subsubmenus;
submodel = g_object_ref (model);
if (path == NULL)
return submodel;
submenus = g_strdup (path);
subsubmenus = submenus;
while (subsubmenus && strlen (subsubmenus) > 0)
{
gint n_items;
gint i;
submenu = subsubmenus;
while (*submenu == '/')
submenu++;
subsubmenus = strstr (submenu, "/");
if (subsubmenus)
*(subsubmenus++) = '\0';
if (strlen (submenu) == 0)
break;
n_items = g_menu_model_get_n_items (G_MENU_MODEL (submodel));
for (i = 0; i < n_items; i++)
{
GMenuModel *subsubmodel;
gchar *label = NULL;
gchar *canon_label = NULL;
const gchar *en_label;
subsubmodel = g_menu_model_get_item_link (G_MENU_MODEL (submodel), i, G_MENU_LINK_SUBMENU);
g_menu_model_get_item_attribute (G_MENU_MODEL (submodel), i, G_MENU_ATTRIBUTE_LABEL, "s", &label);
if (label)
{
en_label = g_object_get_data (G_OBJECT (subsubmodel),
"gimp-menu-model-en-label");
canon_label = gimp_utils_make_canonical_menu_label (en_label);
}
if (subsubmodel && g_strcmp0 (canon_label, submenu) == 0)
{
g_object_unref (submodel);
submodel = GIMP_MENU_MODEL (subsubmodel);
g_free (label);
g_free (canon_label);
break;
}
g_clear_object (&subsubmodel);
g_free (label);
g_free (canon_label);
}
g_return_val_if_fail (i < n_items, NULL);
}
g_free (submenus);
return submodel;
}
const gchar *
gimp_menu_model_get_path (GimpMenuModel *model)
{
return model->priv->path;
}
void
gimp_menu_model_set_title (GimpMenuModel *model,
const gchar *path,
const gchar *title)
{
GMenuItem *item;
GimpMenuModel *submenu = NULL;
item = gimp_menu_model_get_menu_item_rec (model, path, &submenu, NULL);
if (item != NULL)
{
g_menu_item_set_label (item, title);
g_object_notify (G_OBJECT (submenu), "title");
}
}
void
gimp_menu_model_set_color (GimpMenuModel *model,
const gchar *path,
GeglColor *color)
{
GMenuItem *item;
GimpMenuModel *submenu = NULL;
item = gimp_menu_model_get_menu_item_rec (model, path, &submenu, NULL);
if (item != NULL)
{
g_clear_object (&submenu->priv->submenu_color);
submenu->priv->submenu_color = color ? gegl_color_duplicate (color) : NULL;
g_object_notify (G_OBJECT (submenu), "color");
}
}
/* Private functions. */
static GimpMenuModel *
gimp_menu_model_new_section (GimpUIManager *manager,
GMenuModel *model,
const gchar *path)
{
g_return_val_if_fail (GIMP_IS_UI_MANAGER (manager), NULL);
return g_object_new (GIMP_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
"path", path,
"section", TRUE,
NULL);
}
static GimpMenuModel *
gimp_menu_model_new_submenu (GimpUIManager *manager,
GMenuModel *model,
const gchar *path)
{
g_return_val_if_fail (GIMP_IS_UI_MANAGER (manager), NULL);
return g_object_new (GIMP_TYPE_MENU_MODEL,
"manager", manager,
"model", model,
"path", path,
NULL);
}
static void
gimp_menu_model_initialize (GimpMenuModel *model,
GMenuModel *gmodel)
{
gint n_items;
g_return_if_fail (GIMP_IS_MENU_MODEL (model));
g_return_if_fail (gmodel == NULL || G_IS_MENU_MODEL (gmodel));
n_items = gmodel != NULL ? g_menu_model_get_n_items (gmodel) : 0;
for (int i = 0; i < n_items; i++)
{
GMenuModel *subsection;
GMenuModel *submenu;
gchar *label = NULL;
gchar *action_name = NULL;
GMenuItem *item = NULL;
subsection = g_menu_model_get_item_link (G_MENU_MODEL (gmodel), i, G_MENU_LINK_SECTION);
submenu = g_menu_model_get_item_link (G_MENU_MODEL (gmodel), i, G_MENU_LINK_SUBMENU);
g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
G_MENU_ATTRIBUTE_LABEL, "s", &label);
g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
G_MENU_ATTRIBUTE_ACTION, "s", &action_name);
if (subsection != NULL)
{
GimpMenuModel *submodel;
gchar *section_name = NULL;
submodel = gimp_menu_model_new_section (model->priv->manager, subsection,
model->priv->path);
item = g_menu_item_new_section (label, G_MENU_MODEL (submodel));
if (g_menu_model_get_item_attribute (G_MENU_MODEL (gmodel), i,
"section-name", "s", &section_name))
g_hash_table_insert (model->priv->named_sections,
(gpointer) section_name,
item);
g_object_unref (submodel);
}
else if (submenu != NULL && label == NULL)
{
GimpMenuModel *submodel;
GimpAction *action;
gchar *canon_label;
const gchar *group_label;
gchar *path;
g_return_if_fail (action_name != NULL);
action = gimp_ui_manager_find_action (model->priv->manager, NULL, action_name);
/* As a special case, when a submenu has no label, we expect it to
* have an action attribute, which must be for a radio action. In such
* a case, we'll use the radio actions' group label as submenu title.
* See e.g.: menus/gradient-editor-menu.ui
*/
g_return_if_fail (GIMP_IS_RADIO_ACTION (action));
group_label = gimp_radio_action_get_group_label (GIMP_RADIO_ACTION (action));
canon_label = gimp_utils_make_canonical_menu_label (group_label);
path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
g_free (canon_label);
submodel = gimp_menu_model_new_submenu (model->priv->manager, submenu, path);
item = g_menu_item_new_submenu (group_label, G_MENU_MODEL (submodel));
g_signal_connect_object (action, "notify::group-label",
G_CALLBACK (gimp_menu_model_notify_group_label),
item, 0);
g_object_unref (submodel);
g_free (path);
}
else if (submenu != NULL)
{
GimpMenuModel *submodel;
gchar *canon_label;
gchar *path;
const gchar *en_label;
gchar *en_label_copy;
g_return_if_fail (label != NULL);
en_label = g_object_get_data (G_OBJECT (submenu),
"gimp-ui-manager-menu-model-en-label");
g_return_if_fail (en_label != NULL);
canon_label = gimp_utils_make_canonical_menu_label (en_label);
path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
g_free (canon_label);
submodel = gimp_menu_model_new_submenu (model->priv->manager, submenu, path);
item = g_menu_item_new_submenu (label, G_MENU_MODEL (submodel));
en_label_copy = g_strdup (en_label);
g_object_set_data_full (G_OBJECT (submodel), "gimp-menu-model-en-label",
en_label_copy, (GDestroyNotify) g_free);
submodel->priv->submenu_item = item;
g_object_unref (submodel);
g_free (path);
}
else
{
GimpAction *action;
gchar *label_variant = NULL;
item = g_menu_item_new_from_model (G_MENU_MODEL (gmodel), i);
g_return_if_fail (action_name != NULL);
action = gimp_ui_manager_find_action (model->priv->manager, NULL, action_name);
if (model->priv->manager->store_action_paths)
/* Special-case the main menu manager when constructing it as
* this is the only one which should set the menu path.
*/
gimp_action_set_menu_path (action, gimp_menu_model_get_path (model));
g_signal_connect_object (action,
"notify::visible",
G_CALLBACK (gimp_menu_model_action_notify_visible),
model, 0);
g_menu_item_get_attribute (item, "label-variant", "s",
&label_variant);
if (g_strcmp0 (label_variant, "long") == 0)
g_menu_item_set_label (item, gimp_action_get_label (action));
else
g_menu_item_set_label (item, gimp_action_get_short_label (action));
g_signal_connect_object (action,
"notify::short-label",
G_CALLBACK (gimp_menu_model_action_notify_label),
model, 0);
g_signal_connect_object (action,
"notify::label",
G_CALLBACK (gimp_menu_model_action_notify_label),
model, 0);
/* We want GimpRadioAction to be GTK_MENU_TRACKER_ITEM_ROLE_RADIO,
* in order to be displayed as radio menu items (as used to be
* GtkRadioAction) even in the gtk_application_set_menubar() code path.
*
* GTK do this by checking that the item has a "target" parameter,
* which we don't add in our .ui file. Instead let's do this
* programmatically, hence avoiding human errors.
* See: gtk/gtkmenutrackeritem.c
*/
if (GIMP_IS_RADIO_ACTION (action))
{
gint target;
g_object_get (action, "value", &target, NULL);
g_menu_item_set_attribute (item, G_MENU_ATTRIBUTE_TARGET, "i", target);
}
g_free (label_variant);
}
if (item)
model->priv->items = g_list_append (model->priv->items, item);
g_free (label);
g_free (action_name);
g_clear_object (&subsection);
g_clear_object (&submenu);
}
if (! model->priv->is_section)
{
g_signal_connect_object (model->priv->manager, "ui-added",
G_CALLBACK (gimp_menu_model_ui_added),
model, 0);
g_signal_connect_object (model->priv->manager, "ui-removed",
G_CALLBACK (gimp_menu_model_ui_removed),
model, 0);
gimp_ui_manager_foreach_ui (model->priv->manager,
(GimpUIMenuCallback) gimp_menu_model_ui_added,
model);
}
}
/*
* Returns the directory to create as a new submodel to @model, with
* @canonical_path being the fully canonical path name (all slashes unique,
* leading slash, no trailing slash, section name removed and no mnemonic), e.g.
* "/some/path/name" and @mnemonic_path is the fully canonical path name, except
* that it contains mnemonics, e.g. "/_some/p_ath/_name".
*
* The code relies on these canonicalized characteristics so you must make sure
* to feed properly formatted paths.
*
* The return value is the canonical path of the first submenu to create,
* including mnemonics, e.g. if @model was for path "/some", then this function
* returns "p_ath".
*/
static gchar *
gimp_menu_model_handles_subpath (GimpMenuModel *model,
const gchar *canonical_path,
const gchar *mnemonic_path)
{
gchar *new_dir;
gchar *end_new_dir;
gint n_slash = 0;
if (model->priv->path != NULL &&
(! g_str_has_prefix (canonical_path, model->priv->path) ||
canonical_path[strlen (model->priv->path)] != '/'))
{
return FALSE;
}
for (GList *iter = model->priv->items; iter; iter = iter->next)
{
GMenuModel *submenu = NULL;
GMenuModel *subsection = NULL;
GMenuItem *item = iter->data;
subsection = g_menu_item_get_link (item, G_MENU_LINK_SECTION);
if (subsection != NULL)
{
/* Checking if a subsection is a sub-path only gives a partial view of
* the whole menu. So we only handle the negative result (which means
* we found a submenu model which will handle this path instead).
*/
new_dir = gimp_menu_model_handles_subpath (GIMP_MENU_MODEL (subsection),
canonical_path, mnemonic_path);
if (new_dir == NULL)
{
g_clear_object (&subsection);
return NULL;
}
else
{
g_free (new_dir);
}
}
else if ((submenu = g_menu_item_get_link (item, G_MENU_LINK_SUBMENU)) != NULL)
{
gchar *subpath;
subpath = g_strdup_printf ("%s/", GIMP_MENU_MODEL (submenu)->priv->path);
if (g_strcmp0 (canonical_path, GIMP_MENU_MODEL (submenu)->priv->path) == 0 ||
g_str_has_prefix (canonical_path, subpath))
{
/* A submodel will handle the new path. */
g_free (subpath);
g_clear_object (&subsection);
g_clear_object (&submenu);
return NULL;
}
g_free (subpath);
}
g_clear_object (&subsection);
g_clear_object (&submenu);
}
if (model->priv->path != NULL)
{
n_slash = 1;
new_dir = model->priv->path;
while ((new_dir = strstr (new_dir + 1, "/")) != NULL)
n_slash++;
}
new_dir = (gchar *) mnemonic_path + 1;
while (n_slash-- > 0)
new_dir = strstr (new_dir, "/") + 1;
end_new_dir = strstr (new_dir, "/");
if (end_new_dir)
new_dir = g_strndup (new_dir, end_new_dir - new_dir);
else
new_dir = g_strdup (new_dir);
return new_dir;
}
static GMenuItem *
gimp_menu_model_get_item (GimpMenuModel *model,
gint idx)
{
GimpMenuModel *m = GIMP_MENU_MODEL (model);
gint cur = -1;
for (GList *iter = m->priv->items; iter; iter = iter->next)
{
GMenuModel *subsection;
GMenuModel *submenu;
const gchar *action_name = NULL;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
submenu = g_menu_item_get_link (iter->data, G_MENU_LINK_SUBMENU);
if (subsection || submenu)
{
cur++;
}
/* Do not count invisible actions. */
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &action_name))
{
GimpAction *action;
action = gimp_ui_manager_find_action (model->priv->manager, NULL,
action_name);
if (! action)
{
g_critical ("Invalid action '%s'", action_name);
continue;
}
if (gimp_action_is_visible (action))
cur++;
}
g_clear_object (&subsection);
g_clear_object (&submenu);
if (cur == idx)
return iter->data;
}
return NULL;
}
static GMenuItem *
gimp_menu_model_get_menu_item_rec (GimpMenuModel *model,
const gchar *path,
GimpMenuModel **menu,
GMenuItem *item)
{
g_return_val_if_fail (item == model->priv->submenu_item, NULL);
g_return_val_if_fail (menu != NULL && *menu == NULL, NULL);
if (gimp_utils_are_menu_path_identical (path, model->priv->path, NULL, NULL, NULL))
{
*menu = model;
return item;
}
else
{
GList *iter;
GMenuItem *found = NULL;
for (iter = model->priv->items; iter; iter = iter->next)
{
GMenuItem *subitem = iter->data;
GMenuModel *submenu = NULL;
GMenuModel *section = NULL;
submenu = g_menu_item_get_link (subitem, G_MENU_LINK_SUBMENU);
section = g_menu_item_get_link (subitem, G_MENU_LINK_SECTION);
if (section != NULL)
{
found = gimp_menu_model_get_menu_item_rec (GIMP_MENU_MODEL (section), path, menu, NULL);
}
else if (submenu != NULL)
{
gchar *subpath;
subpath = g_strdup_printf ("%s/", GIMP_MENU_MODEL (submenu)->priv->path);
if (g_strcmp0 (path, GIMP_MENU_MODEL (submenu)->priv->path) == 0 ||
g_str_has_prefix (path, subpath))
{
found = gimp_menu_model_get_menu_item_rec (GIMP_MENU_MODEL (submenu), path, menu, subitem);
}
g_free (subpath);
}
g_clear_object (&submenu);
g_clear_object (&section);
if (found != NULL)
break;
}
return found;
}
}
static void
gimp_menu_model_notify_group_label (GimpRadioAction *action,
const GParamSpec *pspec,
GMenuItem *item)
{
g_menu_item_set_label (item, gimp_radio_action_get_group_label (action));
}
static void
gimp_menu_model_action_notify_visible (GimpAction *action,
GParamSpec *pspec,
GimpMenuModel *model)
{
gint pos;
gboolean visible;
pos = gimp_menu_model_get_position (model, gimp_action_get_name (action),
&visible);
if (visible)
g_menu_model_items_changed (G_MENU_MODEL (model), pos, 0, 1);
else
g_menu_model_items_changed (G_MENU_MODEL (model), pos, 1, 0);
}
static void
gimp_menu_model_action_notify_label (GimpAction *action,
GParamSpec *pspec,
GimpMenuModel *model)
{
gchar *label_variant = NULL;
GMenuItem *item;
gint pos;
gboolean visible;
g_return_if_fail (GIMP_IS_ACTION (action));
g_return_if_fail (GIMP_IS_MENU_MODEL (model));
pos = gimp_menu_model_get_position (model, gimp_action_get_name (action), &visible);
item = g_list_nth_data (model->priv->items, pos);
g_return_if_fail (item != NULL);
g_menu_item_get_attribute (item, "label-variant", "s", &label_variant);
if (g_strcmp0 (label_variant, "long") == 0)
g_menu_item_set_label (item, gimp_action_get_label (action));
else
g_menu_item_set_label (item, gimp_action_get_short_label (action));
if (visible)
g_menu_model_items_changed (G_MENU_MODEL (model), pos, 1, 1);
g_free (label_variant);
}
static gboolean
gimp_menu_model_ui_added (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
gboolean top,
GimpMenuModel *model)
{
gchar *canonical_path = NULL;
gchar *mnemonic_path = NULL;
gchar *section_name = NULL;
gboolean added = FALSE;
GimpMenuModel *mod_model = g_object_ref (model);
gchar *new_dir;
if (gimp_utils_are_menu_path_identical (path, model->priv->path, &canonical_path,
&mnemonic_path, &section_name))
{
GApplication *app = model->priv->manager->gimp->app;
GAction *action;
gchar *detailed_action_name;
const gchar *action_prefix = "app";
GMenuItem *item;
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name);
if (action == NULL)
{
action = (GAction *) gimp_ui_manager_find_action (manager, NULL, action_name);
if (action != NULL)
{
GimpActionGroup *group;
group = gimp_action_get_group (GIMP_ACTION (action));
if (group != NULL)
action_prefix = gimp_action_group_get_name (group);
}
}
g_return_val_if_fail (action != NULL, FALSE);
added = TRUE;
if (section_name != NULL)
{
GMenuItem *section_item;
section_item = g_hash_table_lookup (model->priv->named_sections, section_name);
if (section_item)
{
g_object_unref (mod_model);
mod_model = GIMP_MENU_MODEL (g_menu_item_get_link (section_item, G_MENU_LINK_SECTION));
}
}
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (gimp_menu_model_action_notify_visible),
mod_model);
detailed_action_name = g_strdup_printf ("%s.%s", action_prefix, g_action_get_name (action));
item = g_menu_item_new (gimp_action_get_short_label (GIMP_ACTION (action)), detailed_action_name);
/* TODO: add also G_MENU_ATTRIBUTE_ICON attribute? */
g_free (detailed_action_name);
if (model->priv->manager->store_action_paths)
gimp_action_set_menu_path (GIMP_ACTION (action), gimp_menu_model_get_path (model));
if (top)
{
mod_model->priv->items = g_list_prepend (mod_model->priv->items, item);
}
else
{
mod_model->priv->items = g_list_append (mod_model->priv->items, item);
}
if (added)
{
gint position = gimp_menu_model_get_position (mod_model, action_name, NULL);
g_signal_connect_object (action,
"notify::visible",
G_CALLBACK (gimp_menu_model_action_notify_visible),
mod_model, 0);
g_signal_connect_object (action,
"notify::short-label",
G_CALLBACK (gimp_menu_model_action_notify_label),
mod_model, 0);
g_signal_connect_object (action,
"notify::label",
G_CALLBACK (gimp_menu_model_action_notify_label),
mod_model, 0);
g_menu_model_items_changed (G_MENU_MODEL (mod_model), position, 0, 1);
}
else
{
g_object_unref (item);
}
}
else if ((new_dir = gimp_menu_model_handles_subpath (model, canonical_path, mnemonic_path)))
{
GimpMenuModel *submodel;
GMenuItem *item;
gchar *canon_label;
gchar *submodel_path;
canon_label = gimp_utils_make_canonical_menu_label (new_dir);
submodel_path = g_strdup_printf ("%s/%s",
model->priv->path ? model->priv->path : "",
canon_label);
submodel = gimp_menu_model_new_submenu (model->priv->manager, NULL, submodel_path);
item = g_menu_item_new_submenu (new_dir, G_MENU_MODEL (submodel));
if (model->priv->path == NULL)
model->priv->items = g_list_insert (model->priv->items, item,
g_list_length (model->priv->items) - 2);
else
model->priv->items = g_list_append (model->priv->items, item);
g_free (canon_label);
g_object_unref (submodel);
g_free (submodel_path);
g_free (new_dir);
g_menu_model_items_changed (G_MENU_MODEL (model),
gimp_menu_model_get_position (model, NULL, NULL),
1, 0);
}
g_clear_object (&mod_model);
g_free (canonical_path);
g_free (mnemonic_path);
g_free (section_name);
return added;
}
static gboolean
gimp_menu_model_ui_removed (GimpUIManager *manager,
const gchar *path,
const gchar *action_name,
GimpMenuModel *model)
{
gchar *section_name = NULL;
gboolean removed = FALSE;
g_return_val_if_fail (GIMP_IS_MENU_MODEL (model), FALSE);
if (gimp_utils_are_menu_path_identical (path, model->priv->path, NULL, NULL, &section_name))
{
GApplication *app = model->priv->manager->gimp->app;
GMenuItem *item = NULL;
GMenuModel *subsection = NULL;
GAction *action;
GList *iter;
action = g_action_map_lookup_action (G_ACTION_MAP (app), action_name);
removed = TRUE;
for (iter = model->priv->items; iter; iter = iter->next)
{
const gchar *cur_action_name;
subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION);
if (subsection != NULL)
{
if (gimp_menu_model_ui_removed (manager, path, action_name,
GIMP_MENU_MODEL (subsection)))
break;
else
g_clear_object (&subsection);
}
else if (g_menu_item_get_attribute (iter->data,
G_MENU_ATTRIBUTE_ACTION,
"&s", &cur_action_name))
{
gchar *dot = strstr (cur_action_name, ".");
g_return_val_if_fail (dot, FALSE);
/* "action" attribute will start with "app." prefix for main
* actions or with another prefix in some other cases (e.g.
* "tool-options.tool-options-restore-preset-000"). We need to
* clean this up, but for the time being, let's just look at
* what's after the dot.
*/
if (g_strcmp0 (dot + 1, action_name) == 0)
{
item = iter->data;
break;
}
}
}
if (item)
{
gint position;
position = gimp_menu_model_get_position (model, action_name, NULL);
if (action != NULL)
{
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (gimp_menu_model_action_notify_visible),
model);
g_signal_handlers_disconnect_by_func (action,
G_CALLBACK (gimp_menu_model_action_notify_label),
model);
}
g_object_unref (item);
model->priv->items = g_list_delete_link (model->priv->items, iter);
if (! action || gimp_action_is_visible (GIMP_ACTION (action)))
g_menu_model_items_changed (G_MENU_MODEL (model), position, 1, 0);
}
else
{
removed = FALSE;
if (subsection == NULL && ! model->priv->is_section)
g_warning ("%s: no item for action name '%s'.", G_STRFUNC, action_name);
}
/* else: removed in a subsection. */
g_clear_object (&subsection);
}
g_free (section_name);
return removed;
}