/* 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 * * pikauimanager.c * Copyright (C) 2004 Michael Natterer * * 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 #include #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "config/pikaxmlparser.h" #include "core/pika.h" #include "pikaaction.h" #include "pikaactiongroup.h" #include "pikaradioaction.h" #include "pikahelp.h" #include "pikahelp-ids.h" #include "pikamenu.h" #include "pikamenumodel.h" #include "pikamenushell.h" #include "pikatoggleaction.h" #include "pikauimanager.h" #include "pika-intl.h" enum { PROP_0, PROP_NAME, PROP_PIKA }; enum { UPDATE, SHOW_TOOLTIP, HIDE_TOOLTIP, UI_ADDED, UI_REMOVED, LAST_SIGNAL }; typedef struct { gchar *path; gchar *action_name; gboolean top; } PikaUIManagerMenuItem; static void pika_ui_manager_constructed (GObject *object); static void pika_ui_manager_dispose (GObject *object); static void pika_ui_manager_finalize (GObject *object); static void pika_ui_manager_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void pika_ui_manager_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void pika_ui_manager_real_update (PikaUIManager *manager, gpointer update_data); static PikaUIManagerUIEntry * pika_ui_manager_entry_get (PikaUIManager *manager, const gchar *ui_path); static PikaUIManagerUIEntry * pika_ui_manager_entry_ensure (PikaUIManager *manager, const gchar *path); static void pika_ui_manager_store_builder_path (GMenuModel *model, GMenuModel *original); static GtkBuilder * pika_ui_manager_load_builder (const gchar *filename, const gchar *root); static void pika_ui_manager_parse_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error); static void pika_ui_manager_parse_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error); static void pika_ui_manager_parse_ui_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error); static void pika_ui_manager_delete_popdown_data (GtkWidget *widget, PikaUIManager *manager); static void pika_ui_manager_menu_item_free (PikaUIManagerMenuItem *item); static void pika_ui_manager_popup_hidden (GtkMenuShell *popup, gpointer user_data); static gboolean pika_ui_manager_popup_destroy (GtkWidget *popup); static void pika_ui_manager_image_action_added (PikaActionGroup *group, gchar *action_name, PikaUIManager *manager); static void pika_ui_manager_image_action_removed (PikaActionGroup *group, gchar *action_name, PikaUIManager *manager); static void pika_ui_manager_image_accels_changed (PikaAction *action, const gchar **accels, PikaUIManager *manager); G_DEFINE_TYPE (PikaUIManager, pika_ui_manager, PIKA_TYPE_OBJECT) #define parent_class pika_ui_manager_parent_class static guint manager_signals[LAST_SIGNAL] = { 0 }; static void pika_ui_manager_class_init (PikaUIManagerClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = pika_ui_manager_constructed; object_class->dispose = pika_ui_manager_dispose; object_class->finalize = pika_ui_manager_finalize; object_class->set_property = pika_ui_manager_set_property; object_class->get_property = pika_ui_manager_get_property; klass->update = pika_ui_manager_real_update; manager_signals[UPDATE] = g_signal_new ("update", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PikaUIManagerClass, update), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_POINTER); manager_signals[SHOW_TOOLTIP] = g_signal_new ("show-tooltip", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PikaUIManagerClass, show_tooltip), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING); manager_signals[HIDE_TOOLTIP] = g_signal_new ("hide-tooltip", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PikaUIManagerClass, hide_tooltip), NULL, NULL, NULL, G_TYPE_NONE, 0, G_TYPE_NONE); manager_signals[UI_ADDED] = g_signal_new ("ui-added", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PikaUIManagerClass, ui_added), NULL, NULL, NULL, G_TYPE_NONE, 3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); manager_signals[UI_REMOVED] = g_signal_new ("ui-removed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PikaUIManagerClass, ui_removed), NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); g_object_class_install_property (object_class, PROP_NAME, g_param_spec_string ("name", NULL, NULL, NULL, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); g_object_class_install_property (object_class, PROP_PIKA, g_param_spec_object ("pika", NULL, NULL, PIKA_TYPE_PIKA, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); klass->managers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); } static void pika_ui_manager_init (PikaUIManager *manager) { manager->name = NULL; manager->pika = NULL; manager->action_groups = NULL; manager->store_action_paths = FALSE; } static void pika_ui_manager_constructed (GObject *object) { PikaUIManager *manager = PIKA_UI_MANAGER (object); G_OBJECT_CLASS (parent_class)->constructed (object); if (manager->name) { PikaUIManagerClass *manager_class; GList *list; manager_class = PIKA_UI_MANAGER_GET_CLASS (object); list = g_hash_table_lookup (manager_class->managers, manager->name); list = g_list_append (list, manager); g_hash_table_replace (manager_class->managers, g_strdup (manager->name), list); } } static void pika_ui_manager_dispose (GObject *object) { PikaUIManager *manager = PIKA_UI_MANAGER (object); if (manager->name) { PikaUIManagerClass *manager_class; GList *list; manager_class = PIKA_UI_MANAGER_GET_CLASS (object); list = g_hash_table_lookup (manager_class->managers, manager->name); if (list) { list = g_list_remove (list, manager); if (list) g_hash_table_replace (manager_class->managers, g_strdup (manager->name), list); else g_hash_table_remove (manager_class->managers, manager->name); } } G_OBJECT_CLASS (parent_class)->dispose (object); } static void pika_ui_manager_finalize (GObject *object) { PikaUIManager *manager = PIKA_UI_MANAGER (object); GList *list; for (list = manager->registered_uis; list; list = g_list_next (list)) { PikaUIManagerUIEntry *entry = list->data; g_free (entry->ui_path); g_free (entry->basename); g_clear_object (&entry->builder); g_slice_free (PikaUIManagerUIEntry, entry); } g_clear_pointer (&manager->registered_uis, g_list_free); g_clear_pointer (&manager->name, g_free); g_list_free_full (manager->action_groups, g_object_unref); g_list_free_full (manager->ui_items, (GDestroyNotify) pika_ui_manager_menu_item_free); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_ui_manager_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { PikaUIManager *manager = PIKA_UI_MANAGER (object); switch (prop_id) { case PROP_NAME: g_free (manager->name); manager->name = g_value_dup_string (value); break; case PROP_PIKA: manager->pika = g_value_get_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void pika_ui_manager_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { PikaUIManager *manager = PIKA_UI_MANAGER (object); switch (prop_id) { case PROP_NAME: g_value_set_string (value, manager->name); break; case PROP_PIKA: g_value_set_object (value, manager->pika); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void pika_ui_manager_real_update (PikaUIManager *manager, gpointer update_data) { GList *list; for (list = pika_ui_manager_get_action_groups (manager); list; list = g_list_next (list)) { pika_action_group_update (list->data, update_data); } } /** * pika_ui_manager_new: * @pika: the @Pika instance this ui manager belongs to * @name: the UI manager's name. * * Creates a new #PikaUIManager object. * * Returns: the new #PikaUIManager */ PikaUIManager * pika_ui_manager_new (Pika *pika, const gchar *name) { PikaUIManager *manager; g_return_val_if_fail (PIKA_IS_PIKA (pika), NULL); manager = g_object_new (PIKA_TYPE_UI_MANAGER, "name", name, "pika", pika, NULL); return manager; } GList * pika_ui_managers_from_name (const gchar *name) { PikaUIManagerClass *manager_class; GList *list; g_return_val_if_fail (name != NULL, NULL); manager_class = g_type_class_ref (PIKA_TYPE_UI_MANAGER); list = g_hash_table_lookup (manager_class->managers, name); g_type_class_unref (manager_class); return list; } void pika_ui_manager_update (PikaUIManager *manager, gpointer update_data) { g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_signal_emit (manager, manager_signals[UPDATE], 0, update_data); } void pika_ui_manager_add_action_group (PikaUIManager *manager, PikaActionGroup *group) { if (! g_list_find (manager->action_groups, group)) manager->action_groups = g_list_prepend (manager->action_groups, g_object_ref (group)); /* Special-case the UI Manager which should be unique and represent * global application actions. */ if (g_strcmp0 (manager->name, "") == 0) { gchar **actions = g_action_group_list_actions (G_ACTION_GROUP (group)); for (int i = 0; i < g_strv_length (actions); i++) pika_ui_manager_image_action_added (group, actions[i], manager); g_signal_connect_object (group, "action-added", G_CALLBACK (pika_ui_manager_image_action_added), manager, 0); g_signal_connect_object (group, "action-removed", G_CALLBACK (pika_ui_manager_image_action_removed), manager, 0); g_strfreev (actions); } } PikaActionGroup * pika_ui_manager_get_action_group (PikaUIManager *manager, const gchar *name) { GList *list; g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); g_return_val_if_fail (name != NULL, NULL); for (list = pika_ui_manager_get_action_groups (manager); list; list = g_list_next (list)) { PikaActionGroup *group = list->data; if (! strcmp (name, pika_action_group_get_name (group))) return group; } return NULL; } GList * pika_ui_manager_get_action_groups (PikaUIManager *manager) { return manager->action_groups; } PikaMenuModel * pika_ui_manager_get_model (PikaUIManager *manager, const gchar *path) { PikaUIManagerUIEntry *entry; PikaMenuModel *model; GMenuModel *gmodel; PikaMenuModel *submodel; gchar *root; gchar *submenus; root = g_strdup (path); submenus = strstr (root + 1, "/"); if (submenus != NULL) { *submenus = '\0'; if (*(++submenus) == '\0') submenus = NULL; } entry = pika_ui_manager_entry_ensure (manager, path); g_return_val_if_fail (entry != NULL, NULL); if (entry->builder == NULL) { const gchar *menus_path_override = g_getenv ("PIKA_TESTING_MENUS_PATH"); gchar *full_basename; gchar *filename; full_basename = g_strconcat (entry->basename, ".ui", NULL); /* In order for test cases to be able to run without PIKA being * installed yet, allow them to override the menus directory to the * menus dir in the source root */ if (menus_path_override) { GList *paths = pika_path_parse (menus_path_override, 2, FALSE, NULL); GList *list; for (list = paths; list; list = g_list_next (list)) { filename = g_build_filename (list->data, full_basename, NULL); if (! list->next || g_file_test (filename, G_FILE_TEST_EXISTS)) break; g_clear_pointer (&filename, g_free); } g_list_free_full (paths, g_free); } else { filename = g_build_filename (pika_data_directory (), "menus", full_basename, NULL); } g_return_val_if_fail (filename != NULL, NULL); if (manager->pika->be_verbose) g_print ("Loading menu '%s' for %s\n", pika_filename_to_utf8 (filename), entry->ui_path); /* The model is owned by the builder which I have to keep around. */ entry->builder = pika_ui_manager_load_builder (filename, root); g_free (filename); g_free (full_basename); } gmodel = G_MENU_MODEL (gtk_builder_get_object (entry->builder, root)); g_return_val_if_fail (G_IS_MENU (gmodel), NULL); model = pika_menu_model_new (manager, gmodel); submodel = pika_menu_model_get_submodel (model, submenus); g_object_unref (model); g_free (root); return submodel; } void pika_ui_manager_remove_ui (PikaUIManager *manager, const gchar *action_name) { GList *iter = manager->ui_items; g_return_if_fail (action_name != NULL); for (; iter != NULL; iter = iter->next) { PikaUIManagerMenuItem *item = iter->data; if (g_strcmp0 (item->action_name, action_name) == 0) { g_signal_emit (manager, manager_signals[UI_REMOVED], 0, item->path, item->action_name); manager->ui_items = g_list_delete_link (manager->ui_items, iter); pika_ui_manager_menu_item_free (item); break; } } } void pika_ui_manager_remove_uis (PikaUIManager *manager, const gchar *action_name_prefix) { GList *iter = manager->ui_items; while (iter != NULL) { PikaUIManagerMenuItem *item = iter->data; GList *current_iter = iter; /* Increment nearly in case we delete the list item. */ iter = iter->next; if (action_name_prefix == NULL || g_str_has_prefix (item->action_name, action_name_prefix)) { g_signal_emit (manager, manager_signals[UI_REMOVED], 0, item->path, item->action_name); manager->ui_items = g_list_delete_link (manager->ui_items, current_iter); pika_ui_manager_menu_item_free (item); } } } void pika_ui_manager_add_ui (PikaUIManager *manager, const gchar *path, const gchar *action_name, gboolean top) { PikaUIManagerMenuItem *item; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_return_if_fail (path != NULL); g_return_if_fail (action_name != NULL); item = g_slice_new0 (PikaUIManagerMenuItem); item->path = g_strdup (path); item->action_name = g_strdup (action_name); item->top = top; manager->ui_items = g_list_prepend (manager->ui_items, item); g_signal_emit (manager, manager_signals[UI_ADDED], 0, path, action_name, top); } void pika_ui_manager_foreach_ui (PikaUIManager *manager, PikaUIMenuCallback callback, gpointer user_data) { for (GList *iter = g_list_last (manager->ui_items); iter; iter = iter->prev) { PikaUIManagerMenuItem *item = iter->data; callback (manager, item->path, item->action_name, item->top, user_data); } } PikaAction * pika_ui_manager_find_action (PikaUIManager *manager, const gchar *group_name, const gchar *action_name) { PikaActionGroup *group; PikaAction *action = NULL; g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), NULL); g_return_val_if_fail (action_name != NULL, NULL); if (g_strcmp0 (group_name, "app") == 0) { GApplication *app = manager->pika->app; action = (PikaAction *) g_action_map_lookup_action (G_ACTION_MAP (app), action_name); } else if (group_name) { group = pika_ui_manager_get_action_group (manager, group_name); if (group) action = pika_action_group_get_action (group, action_name); } else { GList *list; gchar *dot; dot = strstr (action_name, "."); if (dot == NULL) { /* No group specified. */ for (list = pika_ui_manager_get_action_groups (manager); list; list = g_list_next (list)) { group = list->data; action = pika_action_group_get_action (group, action_name); if (action) break; } } else { gchar *gname; gname = g_strndup (action_name, dot - action_name); action = pika_ui_manager_find_action (manager, gname, dot + 1); g_free (gname); } } return action; } gboolean pika_ui_manager_activate_action (PikaUIManager *manager, const gchar *group_name, const gchar *action_name) { PikaAction *action; g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), FALSE); g_return_val_if_fail (action_name != NULL, FALSE); action = pika_ui_manager_find_action (manager, group_name, action_name); if (action) pika_action_activate (action); return (action != NULL); } gboolean pika_ui_manager_toggle_action (PikaUIManager *manager, const gchar *group_name, const gchar *action_name, gboolean active) { PikaAction *action; g_return_val_if_fail (PIKA_IS_UI_MANAGER (manager), FALSE); g_return_val_if_fail (action_name != NULL, FALSE); action = pika_ui_manager_find_action (manager, group_name, action_name); if (PIKA_IS_TOGGLE_ACTION (action)) pika_toggle_action_set_active (PIKA_TOGGLE_ACTION (action), active ? TRUE : FALSE); return PIKA_IS_TOGGLE_ACTION (action); } void pika_ui_manager_ui_register (PikaUIManager *manager, const gchar *ui_path, const gchar *basename, PikaUIManagerSetupFunc setup_func) { PikaUIManagerUIEntry *entry; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_return_if_fail (ui_path != NULL); g_return_if_fail (basename != NULL); g_return_if_fail (pika_ui_manager_entry_get (manager, ui_path) == NULL); entry = g_slice_new0 (PikaUIManagerUIEntry); entry->ui_path = g_strdup (ui_path); entry->basename = g_strdup (basename); entry->setup_func = setup_func; entry->setup_done = FALSE; entry->builder = NULL; manager->registered_uis = g_list_prepend (manager->registered_uis, entry); } void pika_ui_manager_ui_popup_at_widget (PikaUIManager *manager, const gchar *ui_path, PikaUIManager *child_ui_manager, const gchar *child_ui_path, GtkWidget *widget, GdkGravity widget_anchor, GdkGravity menu_anchor, const GdkEvent *trigger_event, GDestroyNotify popdown_func, gpointer popdown_data) { PikaMenuModel *model; GtkWidget *menu; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_return_if_fail (ui_path != NULL); g_return_if_fail (GTK_IS_WIDGET (widget)); menu = pika_menu_new (manager); gtk_menu_attach_to_widget (GTK_MENU (menu), widget, NULL); model = pika_ui_manager_get_model (manager, ui_path); g_return_if_fail (model != NULL); pika_menu_shell_fill (PIKA_MENU_SHELL (menu), model, TRUE); g_object_unref (model); if (! menu) return; if (child_ui_manager != NULL && child_ui_path != NULL) { PikaMenuModel *child_model; GtkWidget *child_menu; /* TODO GMenu: the "icon" attribute set in the .ui file should be visible. */ child_model = pika_ui_manager_get_model (child_ui_manager, child_ui_path); child_menu = pika_menu_new (child_ui_manager); pika_menu_shell_fill (PIKA_MENU_SHELL (child_menu), child_model, FALSE); g_object_unref (child_model); pika_menu_merge (PIKA_MENU (menu), PIKA_MENU (child_menu), TRUE); } if (popdown_func && popdown_data) { g_object_set_data_full (G_OBJECT (manager), "popdown-data", popdown_data, popdown_func); g_signal_connect (menu, "selection-done", G_CALLBACK (pika_ui_manager_delete_popdown_data), manager); } gtk_menu_popup_at_widget (GTK_MENU (menu), widget, widget_anchor, menu_anchor, trigger_event); g_signal_connect (menu, "hide", G_CALLBACK (pika_ui_manager_popup_hidden), NULL); } void pika_ui_manager_ui_popup_at_pointer (PikaUIManager *manager, const gchar *ui_path, GtkWidget *attached_widget, const GdkEvent *trigger_event, GDestroyNotify popdown_func, gpointer popdown_data) { PikaMenuModel *model; GtkWidget *menu; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_return_if_fail (ui_path != NULL); model = pika_ui_manager_get_model (manager, ui_path); menu = pika_menu_new (manager); gtk_menu_attach_to_widget (GTK_MENU (menu), attached_widget, NULL); pika_menu_shell_fill (PIKA_MENU_SHELL (menu), model, TRUE); g_object_unref (model); if (! menu) return; if (popdown_func && popdown_data) { g_object_set_data_full (G_OBJECT (manager), "popdown-data", popdown_data, popdown_func); g_signal_connect (menu, "selection-done", G_CALLBACK (pika_ui_manager_delete_popdown_data), manager); } gtk_menu_popup_at_pointer (GTK_MENU (menu), trigger_event); g_signal_connect (menu, "hide", G_CALLBACK (pika_ui_manager_popup_hidden), NULL); } void pika_ui_manager_ui_popup_at_rect (PikaUIManager *manager, const gchar *ui_path, GtkWidget *attached_widget, GdkWindow *window, const GdkRectangle *rect, GdkGravity rect_anchor, GdkGravity menu_anchor, const GdkEvent *trigger_event, GDestroyNotify popdown_func, gpointer popdown_data) { PikaMenuModel *model; GtkWidget *menu; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); g_return_if_fail (ui_path != NULL); model = pika_ui_manager_get_model (manager, ui_path); menu = pika_menu_new (manager); gtk_menu_attach_to_widget (GTK_MENU (menu), attached_widget, NULL); pika_menu_shell_fill (PIKA_MENU_SHELL (menu), model, TRUE); g_object_unref (model); if (! menu) return; if (popdown_func && popdown_data) { g_object_set_data_full (G_OBJECT (manager), "popdown-data", popdown_data, popdown_func); g_signal_connect (menu, "selection-done", G_CALLBACK (pika_ui_manager_delete_popdown_data), manager); } gtk_menu_popup_at_rect (GTK_MENU (menu), window, rect, rect_anchor, menu_anchor, trigger_event); g_signal_connect (menu, "hide", G_CALLBACK (pika_ui_manager_popup_hidden), NULL); } /* private functions */ static PikaUIManagerUIEntry * pika_ui_manager_entry_get (PikaUIManager *manager, const gchar *ui_path) { GList *list; gchar *path; path = g_strdup (ui_path); if (strlen (path) > 1) { gchar *p = strchr (path + 1, '/'); if (p) *p = '\0'; } for (list = manager->registered_uis; list; list = g_list_next (list)) { PikaUIManagerUIEntry *entry = list->data; if (! strcmp (entry->ui_path, path)) { g_free (path); return entry; } } g_free (path); return NULL; } static PikaUIManagerUIEntry * pika_ui_manager_entry_ensure (PikaUIManager *manager, const gchar *path) { PikaUIManagerUIEntry *entry; entry = pika_ui_manager_entry_get (manager, path); if (! entry) { g_warning ("%s: no entry registered for \"%s\"", G_STRFUNC, path); return NULL; } if (entry->setup_func && ! entry->setup_done) { entry->setup_func (manager, entry->ui_path); entry->setup_done = TRUE; } return entry; } static void pika_ui_manager_store_builder_path (GMenuModel *model, GMenuModel *original) { gint n_items = 0; g_return_if_fail (g_menu_model_get_n_items (model) == g_menu_model_get_n_items (original)); n_items = (model != NULL ? g_menu_model_get_n_items (model) : 0); for (gint i = 0; i < n_items; i++) { GMenuModel *subsection; GMenuModel *submenu; GMenuModel *en_submodel = 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); if (subsection != NULL) { en_submodel = g_menu_model_get_item_link (G_MENU_MODEL (original), i, G_MENU_LINK_SECTION); pika_ui_manager_store_builder_path (subsection, en_submodel); } else if (submenu != NULL) { gchar *label = NULL; en_submodel = g_menu_model_get_item_link (G_MENU_MODEL (original), i, G_MENU_LINK_SUBMENU); pika_ui_manager_store_builder_path (submenu, en_submodel); g_menu_model_get_item_attribute (G_MENU_MODEL (model), i, G_MENU_ATTRIBUTE_LABEL, "s", &label); if (label != NULL) { gchar *en_label = NULL; g_menu_model_get_item_attribute (G_MENU_MODEL (original), i, G_MENU_ATTRIBUTE_LABEL, "s", &en_label); g_object_set_data_full (G_OBJECT (submenu), "pika-ui-manager-menu-model-en-label", en_label, (GDestroyNotify) g_free); } g_free (label); } g_clear_object (&submenu); g_clear_object (&subsection); g_clear_object (&en_submodel); } } static GtkBuilder * pika_ui_manager_load_builder (const gchar *filename, const gchar *root) { const GMarkupParser markup_parser = { pika_ui_manager_parse_start_element, pika_ui_manager_parse_end_element, pika_ui_manager_parse_ui_text, NULL, /* passthrough */ NULL /* error */ }; GtkBuilder *builder; GtkBuilder *en_builder; gchar *contents; PikaXmlParser *xml_parser; GString *new_xml; GMenuModel *model; GMenuModel *en_model; GError *error = NULL; /* First load the localized version. */ if (! g_file_get_contents (filename, &contents, NULL, &error)) { g_warning ("%s: failed to read \"%s\"", G_STRFUNC, filename); g_clear_error (&error); return NULL; } builder = gtk_builder_new_from_string (contents, -1); /* Now transform the source XML as non-translatable and load it again. */ new_xml = g_string_new (NULL); xml_parser = pika_xml_parser_new (&markup_parser, new_xml); pika_xml_parser_parse_buffer (xml_parser, contents, -1, &error); pika_xml_parser_free (xml_parser); g_free (contents); contents = g_string_free (new_xml, FALSE); if (error != NULL) { g_warning ("%s: error parsing XML file \"%s\"", G_STRFUNC, filename); g_clear_error (&error); g_free (contents); return builder; } en_builder = gtk_builder_new_from_string (contents, -1); g_free (contents); model = G_MENU_MODEL (gtk_builder_get_object (builder, root)); en_model = G_MENU_MODEL (gtk_builder_get_object (en_builder, root)); pika_ui_manager_store_builder_path (model, en_model); g_clear_object (&en_builder); return builder; } static void pika_ui_manager_parse_start_element (GMarkupParseContext *context, const gchar *element_name, const gchar **attribute_names, const gchar **attribute_values, gpointer user_data, GError **error) { GString *new_xml = user_data; g_string_append_printf (new_xml, "<%s", element_name); for (gint i = 0; attribute_names[i] != NULL; i++) { if (g_strcmp0 (attribute_names[i], "translatable") == 0 || g_strcmp0 (attribute_names[i], "context") == 0) continue; g_string_append_printf (new_xml, " %s=\"%s\"", attribute_names[i], attribute_values[i]); } g_string_append_printf (new_xml, ">"); } static void pika_ui_manager_parse_end_element (GMarkupParseContext *context, const gchar *element_name, gpointer user_data, GError **error) { GString *new_xml = user_data; g_string_append_printf (new_xml, "", element_name); } static void pika_ui_manager_parse_ui_text (GMarkupParseContext *context, const gchar *text, gsize text_len, gpointer user_data, GError **error) { GString *new_xml = user_data; gchar *unchanged = g_markup_escape_text (text, text_len); g_string_append (new_xml, unchanged); g_free (unchanged); } static void pika_ui_manager_delete_popdown_data (GtkWidget *widget, PikaUIManager *manager) { g_signal_handlers_disconnect_by_func (widget, pika_ui_manager_delete_popdown_data, manager); g_object_set_data (G_OBJECT (manager), "popdown-data", NULL); } static void pika_ui_manager_menu_item_free (PikaUIManagerMenuItem *item) { g_free (item->path); g_free (item->action_name); g_slice_free (PikaUIManagerMenuItem, item); } static void pika_ui_manager_popup_hidden (GtkMenuShell *popup, gpointer user_data) { /* Destroying the popup would happen after we finish all other events related * to this popup. Otherwise the action which might have been activated will * not run because it happens after hiding. */ g_idle_add ((GSourceFunc) pika_ui_manager_popup_destroy, g_object_ref (popup)); } static gboolean pika_ui_manager_popup_destroy (GtkWidget *popup) { gtk_menu_detach (GTK_MENU (popup)); g_object_unref (popup); return G_SOURCE_REMOVE; } static void pika_ui_manager_image_action_added (PikaActionGroup *group, gchar *action_name, PikaUIManager *manager) { PikaAction *action; const gchar **accels; g_return_if_fail (PIKA_IS_UI_MANAGER (manager)); /* The action should not already exist in the application. */ g_return_if_fail (g_action_map_lookup_action (G_ACTION_MAP (manager->pika->app), action_name) == NULL); action = pika_action_group_get_action (group, action_name); g_return_if_fail (action != NULL); g_action_map_add_action (G_ACTION_MAP (manager->pika->app), G_ACTION (action)); g_signal_connect_object (action, "accels-changed", G_CALLBACK (pika_ui_manager_image_accels_changed), manager, 0); accels = pika_action_get_accels (action); if (accels && g_strv_length ((gchar **) accels) > 0) pika_ui_manager_image_accels_changed (action, pika_action_get_accels (action), manager); } static void pika_ui_manager_image_action_removed (PikaActionGroup *group, gchar *action_name, PikaUIManager *manager) { GAction *action; action = g_action_map_lookup_action (G_ACTION_MAP (manager->pika->app), action_name); if (action != NULL) { g_action_map_remove_action (G_ACTION_MAP (manager->pika->app), action_name); g_signal_handlers_disconnect_by_func (action, G_CALLBACK (pika_ui_manager_image_accels_changed), manager); } } static void pika_ui_manager_image_accels_changed (PikaAction *action, const gchar **accels, PikaUIManager *manager) { gchar *detailed_action_name; if (PIKA_IS_RADIO_ACTION (action)) { gint value; g_object_get ((GObject *) action, "value", &value, NULL); detailed_action_name = g_strdup_printf ("app.%s(%i)", g_action_get_name (G_ACTION (action)), value); } else { detailed_action_name = g_strdup_printf ("app.%s", g_action_get_name (G_ACTION (action))); } gtk_application_set_accels_for_action (GTK_APPLICATION (manager->pika->app), detailed_action_name, accels ? accels : (const gchar *[]) { NULL }); g_free (detailed_action_name); }