1250 lines
39 KiB
C
1250 lines
39 KiB
C
/* 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 <mitch@gimp.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <gegl.h>
|
|
#include <gtk/gtk.h>
|
|
#include <gdk/gdkkeysyms.h>
|
|
|
|
#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 <Image> UI Manager which should be unique and represent
|
|
* global application actions.
|
|
*/
|
|
if (g_strcmp0 (manager->name, "<Image>") == 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, "</%s>", 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);
|
|
}
|