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