646 lines
17 KiB
C
646 lines
17 KiB
C
/*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) version 3.
|
|
*
|
|
* 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with the program; if not, see <http://www.gnu.org/licenses/>
|
|
*
|
|
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
|
|
*/
|
|
|
|
#include "e-plugin-ui.h"
|
|
|
|
#include <string.h>
|
|
|
|
#define E_PLUGIN_UI_HOOK_GET_PRIVATE(obj) \
|
|
(G_TYPE_INSTANCE_GET_PRIVATE \
|
|
((obj), E_TYPE_PLUGIN_UI_HOOK, EPluginUIHookPrivate))
|
|
|
|
#define E_PLUGIN_UI_DEFAULT_FUNC "e_plugin_ui_init"
|
|
#define E_PLUGIN_UI_HOOK_CLASS_ID "org.gnome.evolution.ui:1.0"
|
|
|
|
struct _EPluginUIHookPrivate {
|
|
|
|
/* Table of GtkUIManager ID's to UI definitions.
|
|
*
|
|
* For example:
|
|
*
|
|
* <hook class="org.gnome.evolution.ui:1.0">
|
|
* <ui-manager id="org.gnome.evolution.foo">
|
|
* ... UI definition ...
|
|
* </ui-manager>
|
|
* </hook>
|
|
*
|
|
* Results in:
|
|
*
|
|
* g_hash_table_insert (
|
|
* ui_definitions,
|
|
* "org.gnome.evolution.foo",
|
|
* "... UI definition ...");
|
|
*
|
|
* See http://library.gnome.org/devel/gtk/unstable/GtkUIManager.html
|
|
* for more information about UI definitions. Note: the <ui> tag is
|
|
* optional.
|
|
*/
|
|
GHashTable *ui_definitions;
|
|
|
|
/* Table of GtkUIManager ID's to callback function names.
|
|
*
|
|
* This stores the optional "callback" attribute in the <ui-manager>
|
|
* element. If not specified, it defaults to "e_plugin_ui_init".
|
|
*
|
|
* This is useful when extending the UI of multiple GtkUIManager IDs
|
|
* from a single plugin.
|
|
*
|
|
* For example:
|
|
*
|
|
* <hook class="org.gnome.evolution.ui:1.0">
|
|
* <ui-manager id="org.gnome.evolution.foo" callback="init_foo">
|
|
* ...
|
|
* </ui-manager>
|
|
* <ui-manager id="org.gnome.evolution.bar" callback="init_bar">
|
|
* ...
|
|
* </ui-manager>
|
|
* </hook>
|
|
*
|
|
* Results in:
|
|
*
|
|
* g_hash_table_insert (
|
|
* callbacks, "org.gnome.evolution.foo", "init_foo");
|
|
*
|
|
* g_hash_table_insert (
|
|
* callbacks, "org.gnome.evolution.bar", "init_bar");
|
|
*/
|
|
GHashTable *callbacks;
|
|
|
|
/* The registry is the heart of EPluginUI. It tracks GtkUIManager
|
|
* instances, GtkUIManager IDs, and UI merge IDs as a hash table of
|
|
* hash tables:
|
|
*
|
|
* GtkUIManager instance -> GtkUIManager ID -> UI Merge ID
|
|
*
|
|
* A GtkUIManager instance and ID form a unique key for looking up
|
|
* UI merge IDs. The reason both are needed is because the same
|
|
* GtkUIManager instance can be registered under multiple IDs.
|
|
*
|
|
* This is done primarily to support shell views, which share a
|
|
* common GtkUIManager instance for a particular shell window.
|
|
* Each shell view registers the same GtkUIManager instance under
|
|
* a unique ID:
|
|
*
|
|
* "org.gnome.evolution.mail" }
|
|
* "org.gnome.evolution.contacts" } aliases for a common
|
|
* "org.gnome.evolution.calendar" } GtkUIManager instance
|
|
* "org.gnome.evolution.memos" }
|
|
* "org.gnome.evolution.tasks" }
|
|
*
|
|
* Note: The shell window also registers the same GtkUIManager
|
|
* instance as "org.gnome.evolution.shell".
|
|
*
|
|
* This way, plugins that extend a shell view's UI will follow the
|
|
* merging and unmerging of the shell view automatically.
|
|
*
|
|
* The presence or absence of GtkUIManager IDs in the registry is
|
|
* significant. Presence of a (instance, ID) pair indicates that
|
|
* UI manager is active, absence indicates inactive. Furthermore,
|
|
* a non-zero merge ID for an active UI manager indicates the
|
|
* plugin is enabled. Zero indicates disabled.
|
|
*
|
|
* Here's a quick scenario to illustrate:
|
|
*
|
|
* Suppose we have a plugin that extends the mail shell view UI.
|
|
* Its EPlugin definition file has this section:
|
|
*
|
|
* <hook class="org.gnome.evolution.ui:1.0">
|
|
* <ui-manager id="org.gnome.evolution.mail">
|
|
* ... UI definition ...
|
|
* </ui-manager>
|
|
* </hook>
|
|
*
|
|
* The plugin is enabled and the active shell view is "mail".
|
|
* Let "ManagerA" denote the common GtkUIManager instance for
|
|
* this shell window. Here's what happens to the registry as
|
|
* the user performs various actions;
|
|
*
|
|
* - Initial State Merge ID
|
|
* V
|
|
* { "ManagerA", { "org.gnome.evolution.mail", 3 } }
|
|
*
|
|
* - User Disables the Plugin
|
|
*
|
|
* { "ManagerA", { "org.gnome.evolution.mail", 0 } }
|
|
*
|
|
* - User Enables the Plugin
|
|
*
|
|
* { "ManagerA", { "org.gnome.evolution.mail", 4 } }
|
|
*
|
|
* - User Switches to Calendar View
|
|
*
|
|
* { "ManagerA", { } }
|
|
*
|
|
* - User Disables the Plugin
|
|
*
|
|
* { "ManagerA", { } }
|
|
*
|
|
* - User Switches to Mail View
|
|
*
|
|
* { "ManagerA", { "org.gnome.evolution.mail", 0 } }
|
|
*
|
|
* - User Enables the Plugin
|
|
*
|
|
* { "ManagerA", { "org.gnome.evolution.mail", 5 } }
|
|
*/
|
|
GHashTable *registry;
|
|
};
|
|
|
|
static gpointer parent_class;
|
|
|
|
static void
|
|
plugin_ui_hook_unregister_manager (EPluginUIHook *hook,
|
|
GtkUIManager *ui_manager)
|
|
{
|
|
GHashTable *registry;
|
|
|
|
/* Note: Manager may already be finalized. */
|
|
registry = hook->priv->registry;
|
|
g_hash_table_remove (registry, ui_manager);
|
|
}
|
|
|
|
static void
|
|
plugin_ui_hook_register_manager (EPluginUIHook *hook,
|
|
GtkUIManager *ui_manager,
|
|
const gchar *id,
|
|
gpointer user_data)
|
|
{
|
|
EPlugin *plugin;
|
|
EPluginUIInitFunc func;
|
|
GHashTable *registry;
|
|
GHashTable *hash_table;
|
|
const gchar *func_name;
|
|
|
|
plugin = ((EPluginHook *) hook)->plugin;
|
|
|
|
hash_table = hook->priv->callbacks;
|
|
func_name = g_hash_table_lookup (hash_table, id);
|
|
|
|
if (func_name == NULL)
|
|
func_name = E_PLUGIN_UI_DEFAULT_FUNC;
|
|
|
|
func = e_plugin_get_symbol (plugin, func_name);
|
|
|
|
if (func == NULL) {
|
|
g_critical (
|
|
"Plugin \"%s\" is missing a function named %s()",
|
|
plugin->name, func_name);
|
|
return;
|
|
}
|
|
|
|
/* Pass the manager and user_data to the plugin's callback function.
|
|
* The plugin should install whatever GtkActions and GtkActionGroups
|
|
* are neccessary to implement the actions in its UI definition. */
|
|
if (!func (ui_manager, user_data))
|
|
return;
|
|
|
|
g_object_weak_ref (
|
|
G_OBJECT (ui_manager), (GWeakNotify)
|
|
plugin_ui_hook_unregister_manager, hook);
|
|
|
|
registry = hook->priv->registry;
|
|
hash_table = g_hash_table_lookup (registry, ui_manager);
|
|
|
|
if (hash_table == NULL) {
|
|
hash_table = g_hash_table_new_full (
|
|
g_str_hash, g_str_equal,
|
|
(GDestroyNotify) g_free,
|
|
(GDestroyNotify) NULL);
|
|
g_hash_table_insert (registry, ui_manager, hash_table);
|
|
}
|
|
}
|
|
|
|
static guint
|
|
plugin_ui_hook_merge_ui (EPluginUIHook *hook,
|
|
GtkUIManager *ui_manager,
|
|
const gchar *id)
|
|
{
|
|
GHashTable *hash_table;
|
|
const gchar *ui_definition;
|
|
guint merge_id;
|
|
GError *error = NULL;
|
|
|
|
hash_table = hook->priv->ui_definitions;
|
|
ui_definition = g_hash_table_lookup (hash_table, id);
|
|
g_return_val_if_fail (ui_definition != NULL, 0);
|
|
|
|
merge_id = gtk_ui_manager_add_ui_from_string (
|
|
ui_manager, ui_definition, -1, &error);
|
|
|
|
if (error != NULL) {
|
|
g_warning ("%s", error->message);
|
|
g_error_free (error);
|
|
}
|
|
|
|
return merge_id;
|
|
}
|
|
|
|
static void
|
|
plugin_ui_enable_manager (EPluginUIHook *hook,
|
|
GtkUIManager *ui_manager,
|
|
const gchar *id)
|
|
{
|
|
GHashTable *hash_table;
|
|
GHashTable *ui_definitions;
|
|
GList *keys;
|
|
|
|
hash_table = hook->priv->registry;
|
|
hash_table = g_hash_table_lookup (hash_table, ui_manager);
|
|
|
|
if (hash_table == NULL)
|
|
return;
|
|
|
|
if (id != NULL)
|
|
keys = g_list_prepend (NULL, (gpointer) id);
|
|
else
|
|
keys = g_hash_table_get_keys (hash_table);
|
|
|
|
ui_definitions = hook->priv->ui_definitions;
|
|
|
|
while (keys != NULL) {
|
|
guint merge_id;
|
|
gpointer data;
|
|
|
|
id = keys->data;
|
|
keys = g_list_delete_link (keys, keys);
|
|
|
|
if (g_hash_table_lookup (ui_definitions, id) == NULL)
|
|
continue;
|
|
|
|
data = g_hash_table_lookup (hash_table, id);
|
|
merge_id = GPOINTER_TO_UINT (data);
|
|
|
|
if (merge_id > 0)
|
|
continue;
|
|
|
|
if (((EPluginHook *) hook)->plugin->enabled)
|
|
merge_id = plugin_ui_hook_merge_ui (
|
|
hook, ui_manager, id);
|
|
|
|
/* Merge ID will be 0 on error, which is what we want. */
|
|
data = GUINT_TO_POINTER (merge_id);
|
|
g_hash_table_insert (hash_table, g_strdup (id), data);
|
|
}
|
|
}
|
|
|
|
static void
|
|
plugin_ui_disable_manager (EPluginUIHook *hook,
|
|
GtkUIManager *ui_manager,
|
|
const gchar *id,
|
|
gboolean remove)
|
|
{
|
|
GHashTable *hash_table;
|
|
GHashTable *ui_definitions;
|
|
GList *keys;
|
|
|
|
hash_table = hook->priv->registry;
|
|
hash_table = g_hash_table_lookup (hash_table, ui_manager);
|
|
|
|
if (hash_table == NULL)
|
|
return;
|
|
|
|
if (id != NULL)
|
|
keys = g_list_prepend (NULL, (gpointer) id);
|
|
else
|
|
keys = g_hash_table_get_keys (hash_table);
|
|
|
|
ui_definitions = hook->priv->ui_definitions;
|
|
|
|
while (keys != NULL) {
|
|
guint merge_id;
|
|
gpointer data;
|
|
|
|
id = keys->data;
|
|
keys = g_list_delete_link (keys, keys);
|
|
|
|
if (g_hash_table_lookup (ui_definitions, id) == NULL)
|
|
continue;
|
|
|
|
data = g_hash_table_lookup (hash_table, id);
|
|
merge_id = GPOINTER_TO_UINT (data);
|
|
|
|
/* Merge ID could be 0 if the plugin is disabled. */
|
|
if (merge_id > 0)
|
|
gtk_ui_manager_remove_ui (ui_manager, merge_id);
|
|
|
|
if (remove)
|
|
g_hash_table_remove (hash_table, id);
|
|
else
|
|
g_hash_table_insert (hash_table, g_strdup (id), NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
plugin_ui_enable_hook (EPluginUIHook *hook)
|
|
{
|
|
GHashTable *hash_table;
|
|
GHashTableIter iter;
|
|
gpointer key;
|
|
|
|
/* Enable all GtkUIManagers for this hook. */
|
|
|
|
hash_table = hook->priv->registry;
|
|
g_hash_table_iter_init (&iter, hash_table);
|
|
|
|
while (g_hash_table_iter_next (&iter, &key, NULL)) {
|
|
GtkUIManager *ui_manager = key;
|
|
plugin_ui_enable_manager (hook, ui_manager, NULL);
|
|
}
|
|
}
|
|
|
|
static void
|
|
plugin_ui_disable_hook (EPluginUIHook *hook)
|
|
{
|
|
GHashTable *hash_table;
|
|
GHashTableIter iter;
|
|
gpointer key;
|
|
|
|
/* Disable all GtkUIManagers for this hook. */
|
|
|
|
hash_table = hook->priv->registry;
|
|
g_hash_table_iter_init (&iter, hash_table);
|
|
|
|
while (g_hash_table_iter_next (&iter, &key, NULL)) {
|
|
GtkUIManager *ui_manager = key;
|
|
plugin_ui_disable_manager (hook, ui_manager, NULL, FALSE);
|
|
}
|
|
}
|
|
|
|
static void
|
|
plugin_ui_hook_finalize (GObject *object)
|
|
{
|
|
EPluginUIHookPrivate *priv;
|
|
GHashTableIter iter;
|
|
gpointer ui_manager;
|
|
|
|
priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (object);
|
|
|
|
/* Remove weak reference callbacks to GtkUIManagers. */
|
|
g_hash_table_iter_init (&iter, priv->registry);
|
|
while (g_hash_table_iter_next (&iter, &ui_manager, NULL))
|
|
g_object_weak_unref (
|
|
G_OBJECT (ui_manager), (GWeakNotify)
|
|
plugin_ui_hook_unregister_manager, object);
|
|
|
|
g_hash_table_destroy (priv->ui_definitions);
|
|
g_hash_table_destroy (priv->callbacks);
|
|
g_hash_table_destroy (priv->registry);
|
|
|
|
/* Chain up to parent's dispose() method. */
|
|
G_OBJECT_CLASS (parent_class)->dispose (object);
|
|
}
|
|
|
|
static gint
|
|
plugin_ui_hook_construct (EPluginHook *hook,
|
|
EPlugin *plugin,
|
|
xmlNodePtr node)
|
|
{
|
|
EPluginUIHookPrivate *priv;
|
|
|
|
priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (hook);
|
|
|
|
/* XXX The EPlugin should be a property of EPluginHookClass.
|
|
* Then it could be passed directly to g_object_new() and
|
|
* we wouldn't have to chain up here. */
|
|
|
|
/* Chain up to parent's construct() method. */
|
|
E_PLUGIN_HOOK_CLASS (parent_class)->construct (hook, plugin, node);
|
|
|
|
for (node = xmlFirstElementChild (node); node != NULL;
|
|
node = xmlNextElementSibling (node)) {
|
|
|
|
xmlNodePtr child;
|
|
xmlBufferPtr buffer;
|
|
GString *content;
|
|
const gchar *temp;
|
|
gchar *callback;
|
|
gchar *id;
|
|
|
|
if (strcmp ((gchar *) node->name, "ui-manager") != 0)
|
|
continue;
|
|
|
|
id = e_plugin_xml_prop (node, "id");
|
|
if (id == NULL) {
|
|
g_warning ("<ui-manager> requires 'id' property");
|
|
continue;
|
|
}
|
|
|
|
callback = e_plugin_xml_prop (node, "callback");
|
|
if (callback != NULL)
|
|
g_hash_table_insert (
|
|
priv->callbacks,
|
|
g_strdup (id), callback);
|
|
|
|
content = g_string_sized_new (1024);
|
|
|
|
/* Extract the XML content below <ui-manager> */
|
|
buffer = xmlBufferCreate ();
|
|
for (child = xmlFirstElementChild (node); child != NULL;
|
|
child = xmlNextElementSibling (child)) {
|
|
|
|
xmlNodeDump (buffer, node->doc, child, 2, 1);
|
|
temp = (const gchar *) xmlBufferContent (buffer);
|
|
g_string_append (content, temp);
|
|
}
|
|
|
|
g_hash_table_insert (
|
|
priv->ui_definitions,
|
|
id, g_string_free (content, FALSE));
|
|
|
|
xmlBufferFree (buffer);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
plugin_ui_hook_enable (EPluginHook *hook,
|
|
gint state)
|
|
{
|
|
if (state)
|
|
plugin_ui_enable_hook (E_PLUGIN_UI_HOOK (hook));
|
|
else
|
|
plugin_ui_disable_hook (E_PLUGIN_UI_HOOK (hook));
|
|
}
|
|
|
|
static void
|
|
plugin_ui_hook_class_init (EPluginUIHookClass *class)
|
|
{
|
|
GObjectClass *object_class;
|
|
EPluginHookClass *plugin_hook_class;
|
|
|
|
parent_class = g_type_class_peek_parent (class);
|
|
g_type_class_add_private (class, sizeof (EPluginUIHookPrivate));
|
|
|
|
object_class = G_OBJECT_CLASS (class);
|
|
object_class->finalize = plugin_ui_hook_finalize;
|
|
|
|
plugin_hook_class = E_PLUGIN_HOOK_CLASS (class);
|
|
plugin_hook_class->id = E_PLUGIN_UI_HOOK_CLASS_ID;
|
|
plugin_hook_class->construct = plugin_ui_hook_construct;
|
|
plugin_hook_class->enable = plugin_ui_hook_enable;
|
|
}
|
|
|
|
static void
|
|
plugin_ui_hook_init (EPluginUIHook *hook)
|
|
{
|
|
GHashTable *ui_definitions;
|
|
GHashTable *callbacks;
|
|
GHashTable *registry;
|
|
|
|
ui_definitions = g_hash_table_new_full (
|
|
g_str_hash, g_str_equal,
|
|
(GDestroyNotify) g_free,
|
|
(GDestroyNotify) g_free);
|
|
|
|
callbacks = g_hash_table_new_full (
|
|
g_str_hash, g_str_equal,
|
|
(GDestroyNotify) g_free,
|
|
(GDestroyNotify) g_free);
|
|
|
|
registry = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
|
|
hook->priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (hook);
|
|
hook->priv->ui_definitions = ui_definitions;
|
|
hook->priv->callbacks = callbacks;
|
|
hook->priv->registry = registry;
|
|
}
|
|
|
|
GType
|
|
e_plugin_ui_hook_get_type (void)
|
|
{
|
|
static GType type = 0;
|
|
|
|
if (G_UNLIKELY (type == 0)) {
|
|
static const GTypeInfo type_info = {
|
|
sizeof (EPluginUIHookClass),
|
|
(GBaseInitFunc) NULL,
|
|
(GBaseFinalizeFunc) NULL,
|
|
(GClassInitFunc) plugin_ui_hook_class_init,
|
|
(GClassFinalizeFunc) NULL,
|
|
NULL, /* class_data */
|
|
sizeof (EPluginUIHook),
|
|
0, /* n_preallocs */
|
|
(GInstanceInitFunc) plugin_ui_hook_init,
|
|
NULL /* value_table */
|
|
};
|
|
|
|
type = g_type_register_static (
|
|
E_TYPE_PLUGIN_HOOK, "EPluginUIHook", &type_info, 0);
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
void
|
|
e_plugin_ui_register_manager (GtkUIManager *ui_manager,
|
|
const gchar *id,
|
|
gpointer user_data)
|
|
{
|
|
GSList *plugin_list;
|
|
|
|
g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
|
|
g_return_if_fail (id != NULL);
|
|
|
|
/* Loop over all installed plugins. */
|
|
plugin_list = e_plugin_list_plugins ();
|
|
while (plugin_list != NULL) {
|
|
EPlugin *plugin = plugin_list->data;
|
|
GSList *iter;
|
|
|
|
plugin_list = g_slist_next (plugin_list);
|
|
|
|
/* Look for hooks of type EPluginUIHook. */
|
|
for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
|
|
EPluginUIHook *hook = iter->data;
|
|
GHashTable *hash_table;
|
|
|
|
if (!E_IS_PLUGIN_UI_HOOK (hook))
|
|
continue;
|
|
|
|
hash_table = hook->priv->ui_definitions;
|
|
|
|
/* Check if the hook has a UI definition
|
|
* for the GtkUIManager being registered. */
|
|
if (g_hash_table_lookup (hash_table, id) == NULL)
|
|
continue;
|
|
|
|
/* Register the manager with the hook. */
|
|
plugin_ui_hook_register_manager (
|
|
hook, ui_manager, id, user_data);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
e_plugin_ui_enable_manager (GtkUIManager *ui_manager,
|
|
const gchar *id)
|
|
{
|
|
GSList *plugin_list;
|
|
|
|
g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
|
|
g_return_if_fail (id != NULL);
|
|
|
|
/* Loop over all installed plugins. */
|
|
plugin_list = e_plugin_list_plugins ();
|
|
while (plugin_list != NULL) {
|
|
EPlugin *plugin = plugin_list->data;
|
|
GSList *iter;
|
|
|
|
plugin_list = g_slist_next (plugin_list);
|
|
|
|
/* Look for hooks of type EPluginUIHook. */
|
|
for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
|
|
EPluginUIHook *hook = iter->data;
|
|
|
|
if (!E_IS_PLUGIN_UI_HOOK (hook))
|
|
continue;
|
|
|
|
plugin_ui_enable_manager (hook, ui_manager, id);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
e_plugin_ui_disable_manager (GtkUIManager *ui_manager,
|
|
const gchar *id)
|
|
{
|
|
GSList *plugin_list;
|
|
|
|
g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
|
|
g_return_if_fail (id != NULL);
|
|
|
|
/* Loop over all installed plugins. */
|
|
plugin_list = e_plugin_list_plugins ();
|
|
while (plugin_list != NULL) {
|
|
EPlugin *plugin = plugin_list->data;
|
|
GSList *iter;
|
|
|
|
plugin_list = g_slist_next (plugin_list);
|
|
|
|
/* Look for hooks of type EPluginUIHook. */
|
|
for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
|
|
EPluginUIHook *hook = iter->data;
|
|
|
|
if (!E_IS_PLUGIN_UI_HOOK (hook))
|
|
continue;
|
|
|
|
plugin_ui_disable_manager (hook, ui_manager, id, TRUE);
|
|
}
|
|
}
|
|
}
|