/* 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 * * pikamenu_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 "libpikabase/pikabase.h" #include "libpikacolor/pikacolor.h" #include "libpikawidgets/pikawidgets.h" #include "core/pika.h" #include "pikaaction.h" #include "pikaactiongroup.h" #include "pikamenumodel.h" #include "pikamenushell.h" #include "pikaradioaction.h" #include "pikauimanager.h" #include "pikawidgets-utils.h" /** * PikaMenuModel: * * PikaMenuModel implements GMenuModel. We initialize an object from another * GMenuModel (usually a GMenu), auto-fill with various data from the * PikaAction, 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 PikaUIManager 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 PikaAction or PikaUIManager enhancements. */ enum { PROP_0, PROP_MANAGER, PROP_MODEL, PROP_PATH, PROP_IS_SECTION, PROP_TITLE, PROP_COLOR, }; struct _PikaMenuModelPrivate { PikaUIManager *manager; GMenuModel *model; gchar *path; gboolean is_section; /* If this PikaMenuModel represents a submenu for a bigger menu, this object * will not be NULL. */ GMenuItem *submenu_item; PikaRGB *submenu_color; GList *items; GHashTable *named_sections; }; static void pika_menu_model_finalize (GObject *object); static void pika_menu_model_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void pika_menu_model_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static GVariant * pika_menu_model_get_item_attribute_value (GMenuModel *model, gint item_index, const gchar *attribute, const GVariantType *expected_type); static void pika_menu_model_get_item_attributes (GMenuModel *model, gint item_index, GHashTable **attributes); static GMenuModel * pika_menu_model_get_item_link (GMenuModel *model, gint item_index, const gchar *link); static void pika_menu_model_get_item_links (GMenuModel *model, gint item_index, GHashTable **links); static gint pika_menu_model_get_n_items (GMenuModel *model); static gint pika_menu_model_get_position (PikaMenuModel *model, const gchar *action_name, gboolean *visible);; static gboolean pika_menu_model_is_mutable (GMenuModel *model); static void pika_menu_model_initialize (PikaMenuModel *model, GMenuModel *gmodel); static gchar * pika_menu_model_handles_subpath (PikaMenuModel *model, const gchar *canonical_path, const gchar *mnemonic_path); static GMenuItem * pika_menu_model_get_item (PikaMenuModel *model, gint idx); static GMenuItem * pika_menu_model_get_menu_item_rec (PikaMenuModel *model, const gchar *path, PikaMenuModel **menu, GMenuItem *item); static void pika_menu_model_notify_group_label (PikaRadioAction *action, const GParamSpec *pspec, GMenuItem *item); static void pika_menu_model_action_notify_visible (PikaAction *action, GParamSpec *pspec, PikaMenuModel *model); static void pika_menu_model_action_notify_label (PikaAction *action, GParamSpec *pspec, GMenuItem *item); static gboolean pika_menu_model_ui_added (PikaUIManager *manager, const gchar *path, const gchar *action_name, gboolean top, PikaMenuModel *model); static gboolean pika_menu_model_ui_removed (PikaUIManager *manager, const gchar *path, const gchar *action_name, PikaMenuModel *model); G_DEFINE_TYPE_WITH_CODE (PikaMenuModel, pika_menu_model, G_TYPE_MENU_MODEL, G_ADD_PRIVATE (PikaMenuModel)) #define parent_class pika_menu_model_parent_class /* Class functions */ static void pika_menu_model_class_init (PikaMenuModelClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GMenuModelClass *model_class = G_MENU_MODEL_CLASS (klass); object_class->finalize = pika_menu_model_finalize; object_class->get_property = pika_menu_model_get_property; object_class->set_property = pika_menu_model_set_property; model_class->get_item_attribute_value = pika_menu_model_get_item_attribute_value; model_class->get_item_attributes = pika_menu_model_get_item_attributes; model_class->get_item_link = pika_menu_model_get_item_link; model_class->get_item_links = pika_menu_model_get_item_links; model_class->get_n_items = pika_menu_model_get_n_items; model_class->is_mutable = pika_menu_model_is_mutable; g_object_class_install_property (object_class, PROP_MANAGER, g_param_spec_object ("manager", NULL, NULL, PIKA_TYPE_UI_MANAGER, PIKA_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, PIKA_PARAM_READWRITE)); g_object_class_install_property (object_class, PROP_PATH, g_param_spec_string ("path", NULL, NULL, NULL, PIKA_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); g_object_class_install_property (object_class, PROP_IS_SECTION, g_param_spec_boolean ("section", NULL, NULL, FALSE, PIKA_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, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); g_object_class_install_property (object_class, PROP_COLOR, pika_param_spec_rgb ("color", NULL, NULL, TRUE, &(PikaRGB) {}, PIKA_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY)); } static void pika_menu_model_init (PikaMenuModel *model) { model->priv = pika_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 pika_menu_model_finalize (GObject *object) { PikaMenuModel *model = PIKA_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_free (model->priv->submenu_color); g_hash_table_destroy (model->priv->named_sections); G_OBJECT_CLASS (parent_class)->finalize (object); } void pika_menu_model_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PikaMenuModel *model = PIKA_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_boxed (value, model->priv->submenu_color); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } void pika_menu_model_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaMenuModel *model = PIKA_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); pika_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: pika_menu_model_set_title (model, model->priv->path, g_value_get_string (value)); break; case PROP_COLOR: pika_menu_model_set_color (model, model->priv->path, g_value_get_boxed (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static GVariant* pika_menu_model_get_item_attribute_value (GMenuModel *model, gint item_index, const gchar *attribute, const GVariantType *expected_type) { PikaMenuModel *m = PIKA_MENU_MODEL (model); GMenuItem *item; item = pika_menu_model_get_item (m, item_index); return g_menu_item_get_attribute_value (item, attribute, expected_type); } static void pika_menu_model_get_item_attributes (GMenuModel *model, gint item_index, GHashTable **attributes) { PikaMenuModel *m = PIKA_MENU_MODEL (model); GMenuItem *item; GVariant *value; item = pika_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* pika_menu_model_get_item_link (GMenuModel *model, gint item_index, const gchar *link) { PikaMenuModel *m = PIKA_MENU_MODEL (model); GMenuItem *item; item = pika_menu_model_get_item (m, item_index); return g_menu_item_get_link (item, link); } static void pika_menu_model_get_item_links (GMenuModel *model, gint item_index, GHashTable **links) { PikaMenuModel *m = PIKA_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 = pika_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 pika_menu_model_get_n_items (GMenuModel *model) { PikaMenuModel *m = PIKA_MENU_MODEL (model); return pika_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 pika_menu_model_get_position (PikaMenuModel *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)) { PikaAction *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 = pika_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 = pika_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 && pika_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 pika_menu_model_is_mutable (GMenuModel* model) { return TRUE; } /* Public functions. */ PikaMenuModel * pika_menu_model_new (PikaUIManager *manager, GMenuModel *model) { g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); return g_object_new (PIKA_TYPE_MENU_MODEL, "manager", manager, "model", model, NULL); } PikaMenuModel * pika_menu_model_get_submodel (PikaMenuModel *model, const gchar *path) { PikaMenuModel *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; 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) canon_label = pika_utils_make_canonical_menu_label (label); if (subsubmodel && g_strcmp0 (canon_label, submenu) == 0) { g_object_unref (submodel); submodel = PIKA_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 * pika_menu_model_get_path (PikaMenuModel *model) { return model->priv->path; } void pika_menu_model_set_title (PikaMenuModel *model, const gchar *path, const gchar *title) { GMenuItem *item; PikaMenuModel *submenu = NULL; item = pika_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 pika_menu_model_set_color (PikaMenuModel *model, const gchar *path, const PikaRGB *color) { GMenuItem *item; PikaMenuModel *submenu = NULL; item = pika_menu_model_get_menu_item_rec (model, path, &submenu, NULL); if (item != NULL) { if (color == NULL) g_clear_pointer (&submenu->priv->submenu_color, g_free); else if (submenu->priv->submenu_color == NULL) submenu->priv->submenu_color = g_new (PikaRGB, 1); if (color != NULL) *submenu->priv->submenu_color = *color; g_object_notify (G_OBJECT (submenu), "color"); } } /* Private functions. */ static PikaMenuModel * pika_menu_model_new_section (PikaUIManager *manager, GMenuModel *model, const gchar *path) { g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); return g_object_new (PIKA_TYPE_MENU_MODEL, "manager", manager, "model", model, "path", path, "section", TRUE, NULL); } static PikaMenuModel * pika_menu_model_new_submenu (PikaUIManager *manager, GMenuModel *model, const gchar *path) { g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); return g_object_new (PIKA_TYPE_MENU_MODEL, "manager", manager, "model", model, "path", path, NULL); } static void pika_menu_model_initialize (PikaMenuModel *model, GMenuModel *gmodel) { gint n_items; g_return_if_fail (PIKA_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) { PikaMenuModel *submodel; gchar *section_name = NULL; submodel = pika_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) { PikaMenuModel *submodel; PikaAction *action; gchar *canon_label; const gchar *group_label; gchar *path; g_return_if_fail (action_name != NULL); action = pika_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 (PIKA_IS_RADIO_ACTION (action)); group_label = pika_radio_action_get_group_label (PIKA_RADIO_ACTION (action)); canon_label = pika_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 = pika_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 (pika_menu_model_notify_group_label), item, 0); g_object_unref (submodel); g_free (path); } else if (submenu != NULL) { PikaMenuModel *submodel; gchar *canon_label; gchar *path; const gchar *en_label; g_return_if_fail (label != NULL); en_label = g_object_get_data (G_OBJECT (submenu), "pika-ui-manager-menu-model-en-label"); g_return_if_fail (en_label != NULL); canon_label = pika_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 = pika_menu_model_new_submenu (model->priv->manager, submenu, path); item = g_menu_item_new_submenu (label, G_MENU_MODEL (submodel)); submodel->priv->submenu_item = item; g_object_unref (submodel); g_free (path); } else { PikaAction *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 = pika_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. */ pika_action_set_menu_path (action, pika_menu_model_get_path (model)); g_signal_connect_object (action, "notify::visible", G_CALLBACK (pika_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, pika_action_get_label (action)); else g_menu_item_set_label (item, pika_action_get_short_label (action)); g_signal_connect_object (action, "notify::short-label", G_CALLBACK (pika_menu_model_action_notify_label), item, 0); g_signal_connect_object (action, "notify::label", G_CALLBACK (pika_menu_model_action_notify_label), item, 0); /* We want PikaRadioAction 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 (PIKA_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 (pika_menu_model_ui_added), model, 0); g_signal_connect_object (model->priv->manager, "ui-removed", G_CALLBACK (pika_menu_model_ui_removed), model, 0); pika_ui_manager_foreach_ui (model->priv->manager, (PikaUIMenuCallback) pika_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 * pika_menu_model_handles_subpath (PikaMenuModel *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 = pika_menu_model_handles_subpath (PIKA_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/", PIKA_MENU_MODEL (submenu)->priv->path); if (g_strcmp0 (canonical_path, PIKA_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 * pika_menu_model_get_item (PikaMenuModel *model, gint idx) { PikaMenuModel *m = PIKA_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)) { PikaAction *action; action = pika_ui_manager_find_action (model->priv->manager, NULL, action_name); if (pika_action_is_visible (action)) cur++; } g_clear_object (&subsection); g_clear_object (&submenu); if (cur == idx) return iter->data; } return NULL; } static GMenuItem * pika_menu_model_get_menu_item_rec (PikaMenuModel *model, const gchar *path, PikaMenuModel **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 (pika_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 = pika_menu_model_get_menu_item_rec (PIKA_MENU_MODEL (section), path, menu, NULL); } else if (submenu != NULL) { gchar *subpath; subpath = g_strdup_printf ("%s/", PIKA_MENU_MODEL (submenu)->priv->path); if (g_strcmp0 (path, PIKA_MENU_MODEL (submenu)->priv->path) == 0 || g_str_has_prefix (path, subpath)) { found = pika_menu_model_get_menu_item_rec (PIKA_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 pika_menu_model_notify_group_label (PikaRadioAction *action, const GParamSpec *pspec, GMenuItem *item) { g_menu_item_set_label (item, pika_radio_action_get_group_label (action)); } static void pika_menu_model_action_notify_visible (PikaAction *action, GParamSpec *pspec, PikaMenuModel *model) { gint pos; gboolean visible; pos = pika_menu_model_get_position (model, pika_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 pika_menu_model_action_notify_label (PikaAction *action, GParamSpec *pspec, GMenuItem *item) { gchar *label_variant = NULL; g_return_if_fail (PIKA_IS_ACTION (action)); g_return_if_fail (G_IS_MENU_ITEM (item)); g_menu_item_get_attribute (item, "label-variant", "s", &label_variant); if (g_strcmp0 (label_variant, "long") == 0) g_menu_item_set_label (item, pika_action_get_label (action)); else g_menu_item_set_label (item, pika_action_get_short_label (action)); g_free (label_variant); } static gboolean pika_menu_model_ui_added (PikaUIManager *manager, const gchar *path, const gchar *action_name, gboolean top, PikaMenuModel *model) { gchar *canonical_path = NULL; gchar *mnemonic_path = NULL; gchar *section_name = NULL; gboolean added = FALSE; PikaMenuModel *mod_model = g_object_ref (model); gchar *new_dir; if (pika_utils_are_menu_path_identical (path, model->priv->path, &canonical_path, &mnemonic_path, §ion_name)) { GApplication *app = model->priv->manager->pika->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 *) pika_ui_manager_find_action (manager, NULL, action_name); if (action != NULL) { PikaActionGroup *group; group = pika_action_get_group (PIKA_ACTION (action)); if (group != NULL) action_prefix = pika_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 = PIKA_MENU_MODEL (g_menu_item_get_link (section_item, G_MENU_LINK_SECTION)); } } g_signal_handlers_disconnect_by_func (action, G_CALLBACK (pika_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 (pika_action_get_short_label (PIKA_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) pika_action_set_menu_path (PIKA_ACTION (action), pika_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 = pika_menu_model_get_position (mod_model, action_name, NULL); g_signal_connect_object (action, "notify::visible", G_CALLBACK (pika_menu_model_action_notify_visible), mod_model, 0); g_signal_connect_object (action, "notify::short-label", G_CALLBACK (pika_menu_model_action_notify_label), item, 0); g_signal_connect_object (action, "notify::label", G_CALLBACK (pika_menu_model_action_notify_label), item, 0); g_menu_model_items_changed (G_MENU_MODEL (mod_model), position, 0, 1); } else { g_object_unref (item); } } else if ((new_dir = pika_menu_model_handles_subpath (model, canonical_path, mnemonic_path))) { PikaMenuModel *submodel; GMenuItem *item; gchar *canon_label; gchar *submodel_path; canon_label = pika_utils_make_canonical_menu_label (new_dir); submodel_path = g_strdup_printf ("%s/%s", model->priv->path ? model->priv->path : "", canon_label); submodel = pika_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), pika_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 pika_menu_model_ui_removed (PikaUIManager *manager, const gchar *path, const gchar *action_name, PikaMenuModel *model) { gchar *section_name = NULL; gboolean removed = FALSE; if (pika_utils_are_menu_path_identical (path, model->priv->path, NULL, NULL, §ion_name)) { GApplication *app = model->priv->manager->pika->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 *action; subsection = g_menu_item_get_link (iter->data, G_MENU_LINK_SECTION); if (subsection != NULL) { if (pika_menu_model_ui_removed (manager, path, action_name, PIKA_MENU_MODEL (subsection))) break; else g_clear_object (&subsection); } else if (g_menu_item_get_attribute (iter->data, G_MENU_ATTRIBUTE_ACTION, "&s", &action) && /* "action" attribute will start with "app." prefix. */ g_strcmp0 (action + 4, action_name) == 0) { item = iter->data; break; } } if (item) { gint position; position = pika_menu_model_get_position (model, action_name, NULL); if (action != NULL) { g_signal_handlers_disconnect_by_func (action, G_CALLBACK (pika_menu_model_action_notify_visible), model); g_signal_handlers_disconnect_by_func (action, G_CALLBACK (pika_menu_model_action_notify_label), item); } g_object_unref (item); model->priv->items = g_list_delete_link (model->priv->items, iter); 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; }