/* 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 . */ #include "config.h" #include #include #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", §ion_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 (§ion); 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, §ion_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, §ion_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; }