1 /*
2 * This program is free software; you can redistribute it and/or modify it
3 * under the terms of the GNU Lesser General Public License as published by
4 * the Free Software Foundation.
5 *
6 * This program is distributed in the hope that it will be useful, but
7 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
8 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
9 * for more details.
10 *
11 * You should have received a copy of the GNU Lesser General Public License
12 * along with this program; if not, see <http://www.gnu.org/licenses/>.
13 *
14 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
15 */
16
17 #include "evolution-config.h"
18
19 #include "e-plugin-ui.h"
20
21 #include <string.h>
22
23 #define E_PLUGIN_UI_HOOK_GET_PRIVATE(obj) \
24 (G_TYPE_INSTANCE_GET_PRIVATE \
25 ((obj), E_TYPE_PLUGIN_UI_HOOK, EPluginUIHookPrivate))
26
27 #define E_PLUGIN_UI_DEFAULT_FUNC "e_plugin_ui_init"
28 #define E_PLUGIN_UI_HOOK_CLASS_ID "org.gnome.evolution.ui:1.0"
29
30 struct _EPluginUIHookPrivate {
31
32 /* Table of GtkUIManager ID's to UI definitions.
33 *
34 * For example:
35 *
36 * <hook class="org.gnome.evolution.ui:1.0">
37 * <ui-manager id="org.gnome.evolution.foo">
38 * ... UI definition ...
39 * </ui-manager>
40 * </hook>
41 *
42 * Results in:
43 *
44 * g_hash_table_insert (
45 * ui_definitions,
46 * "org.gnome.evolution.foo",
47 * "... UI definition ...");
48 *
49 * See http://library.gnome.org/devel/gtk/unstable/GtkUIManager.html
50 * for more information about UI definitions. Note: the <ui> tag is
51 * optional.
52 */
53 GHashTable *ui_definitions;
54
55 /* Table of GtkUIManager ID's to callback function names.
56 *
57 * This stores the optional "callback" attribute in the <ui-manager>
58 * element. If not specified, it defaults to "e_plugin_ui_init".
59 *
60 * This is useful when extending the UI of multiple GtkUIManager IDs
61 * from a single plugin.
62 *
63 * For example:
64 *
65 * <hook class="org.gnome.evolution.ui:1.0">
66 * <ui-manager id="org.gnome.evolution.foo" callback="init_foo">
67 * ...
68 * </ui-manager>
69 * <ui-manager id="org.gnome.evolution.bar" callback="init_bar">
70 * ...
71 * </ui-manager>
72 * </hook>
73 *
74 * Results in:
75 *
76 * g_hash_table_insert (
77 * callbacks, "org.gnome.evolution.foo", "init_foo");
78 *
79 * g_hash_table_insert (
80 * callbacks, "org.gnome.evolution.bar", "init_bar");
81 */
82 GHashTable *callbacks;
83
84 /* The registry is the heart of EPluginUI. It tracks GtkUIManager
85 * instances, GtkUIManager IDs, and UI merge IDs as a hash table of
86 * hash tables:
87 *
88 * GtkUIManager instance -> GtkUIManager ID -> UI Merge ID
89 *
90 * A GtkUIManager instance and ID form a unique key for looking up
91 * UI merge IDs. The reason both are needed is because the same
92 * GtkUIManager instance can be registered under multiple IDs.
93 *
94 * This is done primarily to support shell views, which share a
95 * common GtkUIManager instance for a particular shell window.
96 * Each shell view registers the same GtkUIManager instance under
97 * a unique ID:
98 *
99 * "org.gnome.evolution.mail" }
100 * "org.gnome.evolution.contacts" } aliases for a common
101 * "org.gnome.evolution.calendar" } GtkUIManager instance
102 * "org.gnome.evolution.memos" }
103 * "org.gnome.evolution.tasks" }
104 *
105 * Note: The shell window also registers the same GtkUIManager
106 * instance as "org.gnome.evolution.shell".
107 *
108 * This way, plugins that extend a shell view's UI will follow the
109 * merging and unmerging of the shell view automatically.
110 *
111 * The presence or absence of GtkUIManager IDs in the registry is
112 * significant. Presence of a (instance, ID) pair indicates that
113 * UI manager is active, absence indicates inactive. Furthermore,
114 * a non-zero merge ID for an active UI manager indicates the
115 * plugin is enabled. Zero indicates disabled.
116 *
117 * Here's a quick scenario to illustrate:
118 *
119 * Suppose we have a plugin that extends the mail shell view UI.
120 * Its EPlugin definition file has this section:
121 *
122 * <hook class="org.gnome.evolution.ui:1.0">
123 * <ui-manager id="org.gnome.evolution.mail">
124 * ... UI definition ...
125 * </ui-manager>
126 * </hook>
127 *
128 * The plugin is enabled and the active shell view is "mail".
129 * Let "ManagerA" denote the common GtkUIManager instance for
130 * this shell window. Here's what happens to the registry as
131 * the user performs various actions;
132 *
133 * - Initial State Merge ID
134 * V
135 * { "ManagerA", { "org.gnome.evolution.mail", 3 } }
136 *
137 * - User Disables the Plugin
138 *
139 * { "ManagerA", { "org.gnome.evolution.mail", 0 } }
140 *
141 * - User Enables the Plugin
142 *
143 * { "ManagerA", { "org.gnome.evolution.mail", 4 } }
144 *
145 * - User Switches to Calendar View
146 *
147 * { "ManagerA", { } }
148 *
149 * - User Disables the Plugin
150 *
151 * { "ManagerA", { } }
152 *
153 * - User Switches to Mail View
154 *
155 * { "ManagerA", { "org.gnome.evolution.mail", 0 } }
156 *
157 * - User Enables the Plugin
158 *
159 * { "ManagerA", { "org.gnome.evolution.mail", 5 } }
160 */
161 GHashTable *registry;
162 };
163
G_DEFINE_TYPE(EPluginUIHook,e_plugin_ui_hook,E_TYPE_PLUGIN_HOOK)164 G_DEFINE_TYPE (
165 EPluginUIHook,
166 e_plugin_ui_hook,
167 E_TYPE_PLUGIN_HOOK)
168
169 static void
170 plugin_ui_hook_unregister_manager (EPluginUIHook *hook,
171 GtkUIManager *ui_manager)
172 {
173 GHashTable *registry;
174
175 /* Note: Manager may already be finalized. */
176 registry = hook->priv->registry;
177 g_hash_table_remove (registry, ui_manager);
178 }
179
180 static void
plugin_ui_hook_register_manager(EPluginUIHook * hook,GtkUIManager * ui_manager,const gchar * id,gpointer user_data)181 plugin_ui_hook_register_manager (EPluginUIHook *hook,
182 GtkUIManager *ui_manager,
183 const gchar *id,
184 gpointer user_data)
185 {
186 EPlugin *plugin;
187 EPluginUIInitFunc func;
188 GHashTable *registry;
189 GHashTable *hash_table;
190 const gchar *func_name;
191
192 plugin = ((EPluginHook *) hook)->plugin;
193
194 hash_table = hook->priv->callbacks;
195 func_name = g_hash_table_lookup (hash_table, id);
196
197 if (func_name == NULL)
198 func_name = E_PLUGIN_UI_DEFAULT_FUNC;
199
200 func = e_plugin_get_symbol (plugin, func_name);
201
202 if (func == NULL) {
203 g_critical (
204 "Plugin \"%s\" is missing a function named %s()",
205 plugin->name, func_name);
206 return;
207 }
208
209 /* Pass the manager and user_data to the plugin's callback function.
210 * The plugin should install whatever GtkActions and GtkActionGroups
211 * are neccessary to implement the actions in its UI definition. */
212 if (!func (ui_manager, user_data))
213 return;
214
215 g_object_weak_ref (
216 G_OBJECT (ui_manager), (GWeakNotify)
217 plugin_ui_hook_unregister_manager, hook);
218
219 registry = hook->priv->registry;
220 hash_table = g_hash_table_lookup (registry, ui_manager);
221
222 if (hash_table == NULL) {
223 hash_table = g_hash_table_new_full (
224 g_str_hash, g_str_equal,
225 (GDestroyNotify) g_free,
226 (GDestroyNotify) NULL);
227 g_hash_table_insert (registry, ui_manager, hash_table);
228 }
229 }
230
231 static guint
plugin_ui_hook_merge_ui(EPluginUIHook * hook,GtkUIManager * ui_manager,const gchar * id)232 plugin_ui_hook_merge_ui (EPluginUIHook *hook,
233 GtkUIManager *ui_manager,
234 const gchar *id)
235 {
236 GHashTable *hash_table;
237 const gchar *ui_definition;
238 guint merge_id;
239 GError *error = NULL;
240
241 hash_table = hook->priv->ui_definitions;
242 ui_definition = g_hash_table_lookup (hash_table, id);
243 g_return_val_if_fail (ui_definition != NULL, 0);
244
245 merge_id = gtk_ui_manager_add_ui_from_string (
246 ui_manager, ui_definition, -1, &error);
247
248 if (error != NULL) {
249 g_warning ("%s", error->message);
250 g_error_free (error);
251 }
252
253 return merge_id;
254 }
255
256 static void
plugin_ui_enable_manager(EPluginUIHook * hook,GtkUIManager * ui_manager,const gchar * id)257 plugin_ui_enable_manager (EPluginUIHook *hook,
258 GtkUIManager *ui_manager,
259 const gchar *id)
260 {
261 GHashTable *hash_table;
262 GHashTable *ui_definitions;
263 GList *keys;
264
265 hash_table = hook->priv->registry;
266 hash_table = g_hash_table_lookup (hash_table, ui_manager);
267
268 if (hash_table == NULL)
269 return;
270
271 if (id != NULL)
272 keys = g_list_prepend (NULL, (gpointer) id);
273 else
274 keys = g_hash_table_get_keys (hash_table);
275
276 ui_definitions = hook->priv->ui_definitions;
277
278 while (keys != NULL) {
279 guint merge_id;
280 gpointer data;
281
282 id = keys->data;
283 keys = g_list_delete_link (keys, keys);
284
285 if (g_hash_table_lookup (ui_definitions, id) == NULL)
286 continue;
287
288 data = g_hash_table_lookup (hash_table, id);
289 merge_id = GPOINTER_TO_UINT (data);
290
291 if (merge_id > 0)
292 continue;
293
294 if (((EPluginHook *) hook)->plugin->enabled)
295 merge_id = plugin_ui_hook_merge_ui (
296 hook, ui_manager, id);
297
298 /* Merge ID will be 0 on error, which is what we want. */
299 data = GUINT_TO_POINTER (merge_id);
300 g_hash_table_insert (hash_table, g_strdup (id), data);
301 }
302 }
303
304 static void
plugin_ui_disable_manager(EPluginUIHook * hook,GtkUIManager * ui_manager,const gchar * id,gboolean remove)305 plugin_ui_disable_manager (EPluginUIHook *hook,
306 GtkUIManager *ui_manager,
307 const gchar *id,
308 gboolean remove)
309 {
310 GHashTable *hash_table;
311 GHashTable *ui_definitions;
312 GList *keys;
313
314 hash_table = hook->priv->registry;
315 hash_table = g_hash_table_lookup (hash_table, ui_manager);
316
317 if (hash_table == NULL)
318 return;
319
320 if (id != NULL)
321 keys = g_list_prepend (NULL, (gpointer) id);
322 else
323 keys = g_hash_table_get_keys (hash_table);
324
325 ui_definitions = hook->priv->ui_definitions;
326
327 while (keys != NULL) {
328 guint merge_id;
329 gpointer data;
330
331 id = keys->data;
332 keys = g_list_delete_link (keys, keys);
333
334 if (g_hash_table_lookup (ui_definitions, id) == NULL)
335 continue;
336
337 data = g_hash_table_lookup (hash_table, id);
338 merge_id = GPOINTER_TO_UINT (data);
339
340 /* Merge ID could be 0 if the plugin is disabled. */
341 if (merge_id > 0) {
342 gtk_ui_manager_remove_ui (ui_manager, merge_id);
343 gtk_ui_manager_ensure_update (ui_manager);
344 }
345
346 if (remove)
347 g_hash_table_remove (hash_table, id);
348 else
349 g_hash_table_insert (hash_table, g_strdup (id), NULL);
350 }
351 }
352
353 static void
plugin_ui_enable_hook(EPluginUIHook * hook)354 plugin_ui_enable_hook (EPluginUIHook *hook)
355 {
356 GHashTable *hash_table;
357 GHashTableIter iter;
358 gpointer key;
359
360 /* Enable all GtkUIManagers for this hook. */
361
362 hash_table = hook->priv->registry;
363 g_hash_table_iter_init (&iter, hash_table);
364
365 while (g_hash_table_iter_next (&iter, &key, NULL)) {
366 GtkUIManager *ui_manager = key;
367 plugin_ui_enable_manager (hook, ui_manager, NULL);
368 }
369 }
370
371 static void
plugin_ui_disable_hook(EPluginUIHook * hook)372 plugin_ui_disable_hook (EPluginUIHook *hook)
373 {
374 GHashTable *hash_table;
375 GHashTableIter iter;
376 gpointer key;
377
378 /* Disable all GtkUIManagers for this hook. */
379
380 hash_table = hook->priv->registry;
381 g_hash_table_iter_init (&iter, hash_table);
382
383 while (g_hash_table_iter_next (&iter, &key, NULL)) {
384 GtkUIManager *ui_manager = key;
385 plugin_ui_disable_manager (hook, ui_manager, NULL, FALSE);
386 }
387 }
388
389 static void
plugin_ui_hook_finalize(GObject * object)390 plugin_ui_hook_finalize (GObject *object)
391 {
392 EPluginUIHookPrivate *priv;
393 GHashTableIter iter;
394 gpointer ui_manager;
395
396 priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (object);
397
398 /* Remove weak reference callbacks to GtkUIManagers. */
399 g_hash_table_iter_init (&iter, priv->registry);
400 while (g_hash_table_iter_next (&iter, &ui_manager, NULL))
401 g_object_weak_unref (
402 G_OBJECT (ui_manager), (GWeakNotify)
403 plugin_ui_hook_unregister_manager, object);
404
405 g_hash_table_destroy (priv->ui_definitions);
406 g_hash_table_destroy (priv->callbacks);
407 g_hash_table_destroy (priv->registry);
408
409 /* Chain up to parent's dispose() method. */
410 G_OBJECT_CLASS (e_plugin_ui_hook_parent_class)->dispose (object);
411 }
412
413 static gint
plugin_ui_hook_construct(EPluginHook * hook,EPlugin * plugin,xmlNodePtr node)414 plugin_ui_hook_construct (EPluginHook *hook,
415 EPlugin *plugin,
416 xmlNodePtr node)
417 {
418 EPluginUIHookPrivate *priv;
419
420 priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (hook);
421
422 /* XXX The EPlugin should be a property of EPluginHookClass.
423 * Then it could be passed directly to g_object_new() and
424 * we wouldn't have to chain up here. */
425
426 /* Chain up to parent's construct() method. */
427 E_PLUGIN_HOOK_CLASS (e_plugin_ui_hook_parent_class)->
428 construct (hook, plugin, node);
429
430 for (node = xmlFirstElementChild (node); node != NULL;
431 node = xmlNextElementSibling (node)) {
432
433 xmlNodePtr child;
434 xmlBufferPtr buffer;
435 GString *content;
436 const gchar *temp;
437 gchar *callback;
438 gchar *id;
439
440 if (strcmp ((gchar *) node->name, "ui-manager") != 0)
441 continue;
442
443 id = e_plugin_xml_prop (node, "id");
444 if (id == NULL) {
445 g_warning ("<ui-manager> requires 'id' property");
446 continue;
447 }
448
449 callback = e_plugin_xml_prop (node, "callback");
450 if (callback != NULL)
451 g_hash_table_insert (
452 priv->callbacks,
453 g_strdup (id), callback);
454
455 content = g_string_sized_new (1024);
456
457 /* Extract the XML content below <ui-manager> */
458 buffer = xmlBufferCreate ();
459 for (child = node->children; child != NULL; child = child->next) {
460 xmlNodeDump (buffer, node->doc, child, 2, 1);
461 temp = (const gchar *) xmlBufferContent (buffer);
462 g_string_append (content, temp);
463 }
464
465 g_hash_table_insert (
466 priv->ui_definitions,
467 id, g_string_free (content, FALSE));
468
469 xmlBufferFree (buffer);
470 }
471
472 return 0;
473 }
474
475 static void
plugin_ui_hook_enable(EPluginHook * hook,gint state)476 plugin_ui_hook_enable (EPluginHook *hook,
477 gint state)
478 {
479 if (state)
480 plugin_ui_enable_hook (E_PLUGIN_UI_HOOK (hook));
481 else
482 plugin_ui_disable_hook (E_PLUGIN_UI_HOOK (hook));
483 }
484
485 static void
e_plugin_ui_hook_class_init(EPluginUIHookClass * class)486 e_plugin_ui_hook_class_init (EPluginUIHookClass *class)
487 {
488 GObjectClass *object_class;
489 EPluginHookClass *plugin_hook_class;
490
491 g_type_class_add_private (class, sizeof (EPluginUIHookPrivate));
492
493 object_class = G_OBJECT_CLASS (class);
494 object_class->finalize = plugin_ui_hook_finalize;
495
496 plugin_hook_class = E_PLUGIN_HOOK_CLASS (class);
497 plugin_hook_class->id = E_PLUGIN_UI_HOOK_CLASS_ID;
498 plugin_hook_class->construct = plugin_ui_hook_construct;
499 plugin_hook_class->enable = plugin_ui_hook_enable;
500 }
501
502 static void
e_plugin_ui_hook_init(EPluginUIHook * hook)503 e_plugin_ui_hook_init (EPluginUIHook *hook)
504 {
505 GHashTable *ui_definitions;
506 GHashTable *callbacks;
507 GHashTable *registry;
508
509 ui_definitions = g_hash_table_new_full (
510 g_str_hash, g_str_equal,
511 (GDestroyNotify) g_free,
512 (GDestroyNotify) g_free);
513
514 callbacks = g_hash_table_new_full (
515 g_str_hash, g_str_equal,
516 (GDestroyNotify) g_free,
517 (GDestroyNotify) g_free);
518
519 registry = g_hash_table_new_full (
520 g_direct_hash, g_direct_equal,
521 (GDestroyNotify) NULL,
522 (GDestroyNotify) g_hash_table_destroy);
523
524 hook->priv = E_PLUGIN_UI_HOOK_GET_PRIVATE (hook);
525 hook->priv->ui_definitions = ui_definitions;
526 hook->priv->callbacks = callbacks;
527 hook->priv->registry = registry;
528 }
529
530 void
e_plugin_ui_register_manager(GtkUIManager * ui_manager,const gchar * id,gpointer user_data)531 e_plugin_ui_register_manager (GtkUIManager *ui_manager,
532 const gchar *id,
533 gpointer user_data)
534 {
535 GSList *plugin_list;
536
537 g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
538 g_return_if_fail (id != NULL);
539
540 /* Loop over all installed plugins. */
541 plugin_list = e_plugin_list_plugins ();
542 while (plugin_list != NULL) {
543 EPlugin *plugin = plugin_list->data;
544 GSList *iter;
545
546 plugin_list = g_slist_remove (plugin_list, plugin);
547
548 /* Look for hooks of type EPluginUIHook. */
549 for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
550 EPluginUIHook *hook = iter->data;
551 GHashTable *hash_table;
552
553 if (!E_IS_PLUGIN_UI_HOOK (hook))
554 continue;
555
556 hash_table = hook->priv->ui_definitions;
557
558 /* Check if the hook has a UI definition
559 * for the GtkUIManager being registered. */
560 if (g_hash_table_lookup (hash_table, id) == NULL)
561 continue;
562
563 /* Register the manager with the hook. */
564 plugin_ui_hook_register_manager (
565 hook, ui_manager, id, user_data);
566 }
567
568 g_object_unref (plugin);
569 }
570 }
571
572 void
e_plugin_ui_enable_manager(GtkUIManager * ui_manager,const gchar * id)573 e_plugin_ui_enable_manager (GtkUIManager *ui_manager,
574 const gchar *id)
575 {
576 GSList *plugin_list;
577
578 g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
579 g_return_if_fail (id != NULL);
580
581 /* Loop over all installed plugins. */
582 plugin_list = e_plugin_list_plugins ();
583 while (plugin_list != NULL) {
584 EPlugin *plugin = plugin_list->data;
585 GSList *iter;
586
587 plugin_list = g_slist_remove (plugin_list, plugin);
588
589 /* Look for hooks of type EPluginUIHook. */
590 for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
591 EPluginUIHook *hook = iter->data;
592
593 if (!E_IS_PLUGIN_UI_HOOK (hook))
594 continue;
595
596 plugin_ui_enable_manager (hook, ui_manager, id);
597 }
598
599 g_object_unref (plugin);
600 }
601 }
602
603 void
e_plugin_ui_disable_manager(GtkUIManager * ui_manager,const gchar * id)604 e_plugin_ui_disable_manager (GtkUIManager *ui_manager,
605 const gchar *id)
606 {
607 GSList *plugin_list;
608
609 g_return_if_fail (GTK_IS_UI_MANAGER (ui_manager));
610 g_return_if_fail (id != NULL);
611
612 /* Loop over all installed plugins. */
613 plugin_list = e_plugin_list_plugins ();
614 while (plugin_list != NULL) {
615 EPlugin *plugin = plugin_list->data;
616 GSList *iter;
617
618 plugin_list = g_slist_remove (plugin_list, plugin);
619
620 /* Look for hooks of type EPluginUIHook. */
621 for (iter = plugin->hooks; iter != NULL; iter = iter->next) {
622 EPluginUIHook *hook = iter->data;
623
624 if (!E_IS_PLUGIN_UI_HOOK (hook))
625 continue;
626
627 plugin_ui_disable_manager (hook, ui_manager, id, TRUE);
628 }
629
630 g_object_unref (plugin);
631 }
632 }
633