/* 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.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 "libpikawidgets/pikawidgets.h" #include "core/pika.h" #include "pikaaction.h" #include "pikaenumaction.h" #include "pikahelp-ids.h" #include "pikamenu.h" #include "pikamenumodel.h" #include "pikamenushell.h" #include "pikaprocedureaction.h" #include "pikaradioaction.h" #include "pikauimanager.h" #include "pikawidgets-utils.h" #define PIKA_MENU_ACTION_KEY "pika-menu-action" /** * PikaMenu: * * Our own menu widget. * * We cannot use the simpler gtk_menu_new_from_model() because it lacks * tooltip support and unfortunately GTK does not plan to implement this: * https://gitlab.gnome.org/GNOME/gtk/-/issues/785 * * This is why we need to implement our own PikaMenu subclass. It looks very * minimal right now, but the whole point is that it is also a PikaMenuShell * (where all the real work, with proxy items, syncing with actions and such, * happens). */ struct _PikaMenuPrivate { GTree *submenus; GHashTable *sections; }; static void pika_menu_iface_init (PikaMenuShellInterface *iface); static void pika_menu_finalize (GObject *object); static void pika_menu_append (PikaMenuShell *shell, PikaMenuModel *model); static void pika_menu_add_ui (PikaMenuShell *shell, const gchar **paths, const gchar *action_name, gboolean top); static void pika_menu_remove_ui (PikaMenuShell *shell, const gchar **paths, const gchar *action_name); static void pika_menu_model_deleted (PikaMenuShell *shell); static void pika_menu_add_action (PikaMenu *menu, const gchar *action_name, gboolean long_label, GtkWidget *sibling, gboolean top, GtkRadioMenuItem **group); static void pika_menu_remove_action (PikaMenu *menu, const gchar *action_name); static void pika_menu_append_section (PikaMenu *menu, PikaMenuModel *model, GtkWidget *start_separator); static void pika_menu_section_items_changed (GMenuModel *model, gint position, gint removed, gint added, PikaMenu *menu); static void pika_menu_submenu_notify_title (PikaMenuModel *model, const GParamSpec *pspec, GtkMenuItem *item); static void pika_menu_submenu_notify_color (PikaMenuModel *model, const GParamSpec *pspec, GtkMenuItem *item); static void pika_menu_toggle_item_toggled (GtkWidget *item, GAction *action); static void pika_menu_toggle_action_toggled (PikaAction *action, GtkCheckMenuItem *item); static void pika_menu_action_notify_visible (PikaAction *action, const GParamSpec *pspec, GtkWidget *item); static void pika_menu_help_fun (const gchar *bogus_help_id, gpointer help_data); static void pika_menu_hide_double_separators (PikaMenu *menu); G_DEFINE_TYPE_WITH_CODE (PikaMenu, pika_menu, GTK_TYPE_MENU, G_ADD_PRIVATE (PikaMenu) G_IMPLEMENT_INTERFACE (PIKA_TYPE_MENU_SHELL, pika_menu_iface_init)) #define parent_class pika_menu_parent_class /* Class functions */ static void pika_menu_class_init (PikaMenuClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = pika_menu_finalize; object_class->get_property = pika_menu_shell_get_property; object_class->set_property = pika_menu_shell_set_property; pika_menu_shell_install_properties (object_class); } static void pika_menu_init (PikaMenu *menu) { menu->priv = pika_menu_get_instance_private (menu); menu->priv->submenus = g_tree_new_full ((GCompareDataFunc) g_strcmp0, NULL, g_free, NULL); menu->priv->sections = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL); pika_menu_shell_init (PIKA_MENU_SHELL (menu)); pika_help_connect (GTK_WIDGET (menu), pika_menu_help_fun, PIKA_HELP_MAIN, menu, NULL); } static void pika_menu_iface_init (PikaMenuShellInterface *iface) { iface->append = pika_menu_append; iface->add_ui = pika_menu_add_ui; iface->remove_ui = pika_menu_remove_ui; iface->model_deleted = pika_menu_model_deleted; } static void pika_menu_finalize (GObject *object) { PikaMenu *menu = PIKA_MENU (object); g_clear_pointer (&menu->priv->submenus, g_tree_unref); g_hash_table_unref (menu->priv->sections); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_menu_append (PikaMenuShell *shell, PikaMenuModel *model) { static GtkRadioMenuItem *group = NULL; PikaMenu *menu = PIKA_MENU (shell); PikaUIManager *manager = pika_menu_shell_get_manager (PIKA_MENU_SHELL (shell)); gint n_items; g_return_if_fail (GTK_IS_CONTAINER (shell)); n_items = g_menu_model_get_n_items (G_MENU_MODEL (model)); for (gint i = 0; i < n_items; i++) { GMenuModel *subsection; GMenuModel *submenu; gchar *label = NULL; gchar *action_name = NULL; subsection = g_menu_model_get_item_link (G_MENU_MODEL (model), i, G_MENU_LINK_SECTION); submenu = g_menu_model_get_item_link (G_MENU_MODEL (model), i, G_MENU_LINK_SUBMENU); g_menu_model_get_item_attribute (G_MENU_MODEL (model), i, G_MENU_ATTRIBUTE_LABEL, "s", &label); g_menu_model_get_item_attribute (G_MENU_MODEL (model), i, G_MENU_ATTRIBUTE_ACTION, "s", &action_name); if (subsection != NULL) { GtkWidget *item; group = NULL; item = gtk_separator_menu_item_new (); gtk_container_add (GTK_CONTAINER (shell), item); gtk_widget_show (item); /* Don't use pika_menu_shell_append() here because we don't want to * override the main model for this menu. * Instead we keep track of each subsection model and their position. */ pika_menu_append_section (menu, PIKA_MENU_MODEL (subsection), item); item = gtk_separator_menu_item_new (); gtk_container_add (GTK_CONTAINER (shell), item); gtk_widget_show (item); } else if (submenu != NULL) { GtkWidget *subcontainer; GtkWidget *item; group = NULL; /* I don't show the item on purpose because * pika_menu_append() will show the parent item if any of * the added actions are visible. */ item = gtk_menu_item_new_with_mnemonic (label); gtk_container_add (GTK_CONTAINER (shell), item); subcontainer = pika_menu_new (manager); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), subcontainer); pika_menu_shell_append (PIKA_MENU_SHELL (subcontainer), PIKA_MENU_MODEL (submenu)); gtk_widget_show (subcontainer); g_tree_insert (menu->priv->submenus, pika_utils_make_canonical_menu_label (label), subcontainer); g_signal_connect (submenu, "notify::title", G_CALLBACK (pika_menu_submenu_notify_title), item); g_signal_connect (submenu, "notify::color", G_CALLBACK (pika_menu_submenu_notify_color), item); } else { gchar *label_variant = NULL; g_return_if_fail (action_name != NULL); g_menu_model_get_item_attribute (G_MENU_MODEL (model), i, "label-variant", "s", &label_variant); pika_menu_add_action (menu, action_name, /* By default, we use the short label in menus, * unless "label-variant" attribute is set to * "long". */ g_strcmp0 (label_variant, "long") == 0, NULL, FALSE, &group); g_free (label_variant); } g_free (label); g_free (action_name); g_clear_object (&submenu); g_clear_object (&subsection); } pika_menu_hide_double_separators (menu); } static void pika_menu_add_ui (PikaMenuShell *shell, const gchar **paths, const gchar *action_name, gboolean top) { PikaMenu *menu = PIKA_MENU (shell); PikaUIManager *manager = pika_menu_shell_get_manager (PIKA_MENU_SHELL (shell)); GtkWidget *submenu; g_return_if_fail (paths != NULL && paths[0] != NULL); submenu = g_tree_lookup (menu->priv->submenus, paths[0]); if (submenu == NULL) { GtkWidget *item; item = gtk_menu_item_new_with_mnemonic (paths[0]); gtk_container_add (GTK_CONTAINER (shell), item); submenu = pika_menu_new (manager); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); gtk_widget_show (submenu); g_tree_insert (menu->priv->submenus, g_strdup (paths[0]), submenu); } pika_menu_add_ui (PIKA_MENU_SHELL (submenu), paths + 1, action_name, top); pika_menu_hide_double_separators (menu); } static void pika_menu_remove_ui (PikaMenuShell *shell, const gchar **paths, const gchar *action_name) { PikaMenu *menu = PIKA_MENU (shell); g_return_if_fail (paths != NULL); if (paths[0] == NULL) { pika_menu_remove_action (menu, action_name); } else { GtkWidget *submenu = NULL; submenu = g_tree_lookup (menu->priv->submenus, paths[0]); g_return_if_fail (submenu != NULL); pika_menu_remove_ui (PIKA_MENU_SHELL (submenu), paths + 1, action_name); } pika_menu_hide_double_separators (menu); } static void pika_menu_model_deleted (PikaMenuShell *shell) { /* This will unref the sub-models, hence will disconnect the "items-changed" * signal handlers. */ g_hash_table_remove_all (PIKA_MENU (shell)->priv->sections); } /* Public functions */ GtkWidget * pika_menu_new (PikaUIManager *manager) { g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); return g_object_new (PIKA_TYPE_MENU, "manager", manager, NULL); } void pika_menu_merge (PikaMenu *menu, PikaMenu *menu2, gboolean top) { GList *children; GList *iter; children = gtk_container_get_children (GTK_CONTAINER (menu2)); iter = top ? g_list_last (children) : children; for (; iter; iter = top ? iter->prev : iter->next) { GtkWidget *item = iter->data; g_object_ref (item); gtk_container_remove (GTK_CONTAINER (menu2), item); if (top) gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); else gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); g_object_unref (item); } gtk_widget_destroy (GTK_WIDGET (menu2)); g_list_free (children); } /* Private functions */ static void pika_menu_add_action (PikaMenu *menu, const gchar *action_name, gboolean long_label, GtkWidget *sibling, gboolean top, GtkRadioMenuItem **group) { PikaUIManager *manager; PikaAction *action; const gchar *action_label; GtkWidget *item; gboolean visible; g_return_if_fail (PIKA_IS_MENU (menu)); manager = pika_menu_shell_get_manager (PIKA_MENU_SHELL (menu)); action = pika_ui_manager_find_action (manager, NULL, action_name); g_return_if_fail (PIKA_IS_ACTION (action)); if (long_label) action_label = pika_action_get_label (action); else action_label = pika_action_get_short_label (action); g_return_if_fail (action_label != NULL); if (PIKA_IS_TOGGLE_ACTION (action)) { if (PIKA_IS_RADIO_ACTION (action)) item = gtk_radio_menu_item_new_with_mnemonic_from_widget (group ? *group : NULL, action_label); else item = gtk_check_menu_item_new_with_mnemonic (action_label); gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (item), pika_toggle_action_get_active (PIKA_TOGGLE_ACTION (action))); if (group) { if (PIKA_IS_RADIO_ACTION (action)) *group = GTK_RADIO_MENU_ITEM (item); else *group = NULL; } g_signal_connect (item, "toggled", G_CALLBACK (pika_menu_toggle_item_toggled), action); g_signal_connect_object (action, "toggled", G_CALLBACK (pika_menu_toggle_action_toggled), item, 0); } else if (PIKA_IS_PROCEDURE_ACTION (action) || PIKA_IS_ENUM_ACTION (action)) { item = gtk_menu_item_new_with_mnemonic (action_label); if (group) *group = NULL; g_signal_connect_swapped (item, "activate", G_CALLBACK (pika_action_activate), action); } else { item = gtk_menu_item_new_with_mnemonic (action_label); if (group) *group = NULL; g_signal_connect_swapped (item, "activate", G_CALLBACK (pika_action_activate), action); } pika_action_set_proxy (action, item); g_object_set_data (G_OBJECT (item), PIKA_MENU_ACTION_KEY, action); if (sibling) { GList *children; gint position = 0; /* I am assuming that the order of the children list reflects the * position, though it is not clearly specified in the function docs. Yet * I could find no other function giving me the position of some child in * a container. */ children = gtk_container_get_children (GTK_CONTAINER (menu)); for (GList *iter = children; iter; iter = iter->next) { if (iter->data == sibling) break; position++; } if (! top) position++; gtk_menu_shell_insert (GTK_MENU_SHELL (menu), item, position); g_list_free (children); } else { if (top) gtk_menu_shell_prepend (GTK_MENU_SHELL (menu), item); else gtk_menu_shell_append (GTK_MENU_SHELL (menu), item); } visible = pika_action_is_visible (action); gtk_widget_set_visible (item, visible); if (visible && GTK_IS_MENU (menu)) { GtkWidget *parent = GTK_WIDGET (menu); while (parent != NULL && PIKA_IS_MENU (parent)) { /* Note that this is not the container we must show, but the menu item * attached to the parent, in order not to leave empty submenus. */ GtkWidget *menu_item = gtk_menu_get_attach_widget (GTK_MENU (parent)); if (menu_item == NULL) break; if (G_TYPE_FROM_INSTANCE (menu_item) == GTK_TYPE_MENU_ITEM) gtk_widget_show (menu_item); parent = gtk_widget_get_parent (menu_item); } } g_signal_connect_object (action, "notify::visible", G_CALLBACK (pika_menu_action_notify_visible), item, 0); } static void pika_menu_remove_action (PikaMenu *menu, const gchar *action_name) { PikaUIManager *manager; GList *children; PikaAction *action; g_return_if_fail (PIKA_IS_MENU (menu)); manager = pika_menu_shell_get_manager (PIKA_MENU_SHELL (menu)); action = pika_ui_manager_find_action (manager, NULL, action_name); g_return_if_fail (PIKA_IS_ACTION (action)); children = gtk_container_get_children (GTK_CONTAINER (menu)); for (GList *iter = children; iter; iter = iter->next) { GtkWidget *child = iter->data; PikaAction *item_action; item_action = g_object_get_data (G_OBJECT (child), PIKA_MENU_ACTION_KEY); if (item_action == action) { gtk_widget_destroy (child); break; } } g_list_free (children); } static void pika_menu_append_section (PikaMenu *menu, PikaMenuModel *model, GtkWidget *start_separator) { g_hash_table_insert (menu->priv->sections, g_object_ref (model), start_separator); pika_menu_append (PIKA_MENU_SHELL (menu), model); g_signal_connect_object (model, "items-changed", G_CALLBACK (pika_menu_section_items_changed), menu, 0); } static void pika_menu_section_items_changed (GMenuModel *model, gint position, gint removed, gint added, PikaMenu *menu) { GList *children; GList *iter; GtkWidget *separator; gboolean found = FALSE; gint count = position; gint real_pos = 0; separator = g_hash_table_lookup (menu->priv->sections, model); g_return_if_fail (separator != NULL); children = gtk_container_get_children (GTK_CONTAINER (menu)); for (iter = children; iter; iter = iter->next) { real_pos++; if (! found) { if (iter->data == separator) found = TRUE; continue; } if (count > 0) { /* Assume we don't have sections within a section, i.e. in particular * we don't have more separators, which would mess the count! */ count--; continue; } else if (removed > 0) { gtk_widget_destroy (iter->data); removed--; continue; } else { break; } } while (added > 0) { gchar *action_name = NULL; gchar *label_variant = NULL; g_menu_model_get_item_attribute (G_MENU_MODEL (model), position, G_MENU_ATTRIBUTE_ACTION, "s", &action_name); g_menu_model_get_item_attribute (G_MENU_MODEL (model), position, "label-variant", "s", &label_variant); g_return_if_fail (action_name != NULL); pika_menu_add_action (menu, action_name, g_strcmp0 (label_variant, "long") == 0, iter ? iter->data : NULL, iter ? TRUE : FALSE, NULL); g_free (action_name); g_free (label_variant); added--; position++; } g_list_free (children); pika_menu_hide_double_separators (menu); } static void pika_menu_submenu_notify_title (PikaMenuModel *model, const GParamSpec *pspec, GtkMenuItem *item) { gchar *title; g_object_get (model, "title", &title, NULL); gtk_menu_item_set_label (item, title); g_free (title); } static void pika_menu_submenu_notify_color (PikaMenuModel *model, const GParamSpec *pspec, GtkMenuItem *item) { PikaRGB *color = NULL; GtkWidget *image = NULL; gint width, height; g_object_get (model, "color", &color, NULL); if (color) { image = pika_color_area_new (color, PIKA_COLOR_AREA_SMALL_CHECKS, 0); pika_color_area_set_draw_border (PIKA_COLOR_AREA (image), TRUE); /* TODO: the color area should be color-managed. */ /*pika_color_area_set_color_config (PIKA_COLOR_AREA (image),*/ /*pika->config->color_management);*/ gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, &height); gtk_widget_set_size_request (image, width, height); gtk_widget_show (image); } pika_menu_item_set_image (item, image, NULL); g_free (color); } static void pika_menu_toggle_item_toggled (GtkWidget *item, GAction *action) { gboolean active = gtk_check_menu_item_get_active (GTK_CHECK_MENU_ITEM (item)); g_signal_handlers_block_by_func (action, G_CALLBACK (pika_menu_toggle_action_toggled), item); pika_toggle_action_set_active (PIKA_TOGGLE_ACTION (action), active); g_signal_handlers_unblock_by_func (action, G_CALLBACK (pika_menu_toggle_action_toggled), item); } static void pika_menu_toggle_action_toggled (PikaAction *action, GtkCheckMenuItem *item) { gboolean active = pika_toggle_action_get_active (PIKA_TOGGLE_ACTION (action)); g_signal_handlers_block_by_func (item, G_CALLBACK (pika_menu_toggle_item_toggled), action); gtk_check_menu_item_set_active (item, active); g_signal_handlers_unblock_by_func (item, G_CALLBACK (pika_menu_toggle_item_toggled), action); } static void pika_menu_action_notify_visible (PikaAction *action, const GParamSpec *pspec, GtkWidget *item) { GtkWidget *container; gtk_widget_set_visible (item, pika_action_is_visible (action)); container = gtk_widget_get_parent (item); if (pika_action_is_visible (PIKA_ACTION (action))) { GtkWidget *widget = gtk_menu_get_attach_widget (GTK_MENU (container)); /* We must show the GtkMenuItem associated as submenu to the parent * container. */ if (G_TYPE_FROM_INSTANCE (widget) == GTK_TYPE_MENU_ITEM) gtk_widget_show (widget); } else { GList *children = gtk_container_get_children (GTK_CONTAINER (container)); gboolean all_invisible = TRUE; for (GList *iter = children; iter; iter = iter->next) { if (gtk_widget_get_visible (iter->data)) { all_invisible = FALSE; break; } } g_list_free (children); if (all_invisible) { GtkWidget *widget; /* No need to leave empty submenus. */ widget = gtk_menu_get_attach_widget (GTK_MENU (container)); if (G_TYPE_FROM_INSTANCE (widget) == GTK_TYPE_MENU_ITEM) gtk_widget_hide (widget); } } pika_menu_hide_double_separators (PIKA_MENU (container)); } static void pika_menu_help_fun (const gchar *bogus_help_id, gpointer help_data) { gchar *help_id = NULL; PikaMenu *menu; Pika *pika; GtkWidget *item; gchar *help_domain = NULL; gchar *help_string = NULL; gchar *domain_separator; g_return_if_fail (PIKA_IS_MENU (help_data)); menu = PIKA_MENU (help_data); pika = pika_menu_shell_get_manager (PIKA_MENU_SHELL (menu))->pika; g_return_if_fail (PIKA_IS_PIKA (pika)); item = gtk_menu_shell_get_selected_item (GTK_MENU_SHELL (menu)); if (item) help_id = g_object_get_qdata (G_OBJECT (item), PIKA_HELP_ID); if (help_id == NULL || strlen (help_id) == 0) help_id = (gchar *) bogus_help_id; help_id = g_strdup (help_id); domain_separator = strchr (help_id, '?'); if (domain_separator) { *domain_separator = '\0'; help_domain = g_strdup (help_id); help_string = g_strdup (domain_separator + 1); } else { help_string = g_strdup (help_id); } pika_help (pika, NULL, help_domain, help_string); g_free (help_domain); g_free (help_string); g_free (help_id); } /* With successive sections, we will end up with double separators (end one then * start one of the next section). Moreover sometimes, empty sections (e.g. * because items are expected to be added later) would make even 3 to 4 * separators next to each other. This renders very ugly. We need to call this * function to hide and show separators after changes. * * This also hides start and end separators in the menu. */ static void pika_menu_hide_double_separators (PikaMenu *menu) { GList *children; GList *iter; GtkWidget *prev_item = NULL; children = gtk_container_get_children (GTK_CONTAINER (menu)); for (iter = children; iter; iter = iter->next) { GtkWidget *item = iter->data; if (GTK_IS_SEPARATOR_MENU_ITEM (item)) { if (prev_item == NULL || GTK_IS_SEPARATOR_MENU_ITEM (prev_item)) { gtk_widget_hide (item); } else { gtk_widget_show (item); prev_item = item; } } else if (gtk_widget_get_visible (item)) { prev_item = item; } } if (prev_item != NULL && GTK_IS_SEPARATOR_MENU_ITEM (prev_item)) gtk_widget_hide (prev_item); g_list_free (children); }