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