[libyelp/yelp-view] Use GAppLaunchContext instead of gdk_spawn
[yelp.git] / libyelp / yelp-help-list.c
blob7b177981faf98fd878ea1a6110e66d1bcc09cec2
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * Copyright (C) 2010 Shaun McCance <shaunm@gnome.org>
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the
8 * License, or (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * General Public License for more details.
15 * You should have received a copy of the GNU General Public
16 * License along with this program; if not, write to the
17 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
20 * Author: Shaun McCance <shaunm@gnome.org>
23 #ifdef HAVE_CONFIG_H
24 #include <config.h>
25 #endif
27 #include <gio/gio.h>
28 #include <gio/gdesktopappinfo.h>
29 #include <glib/gi18n.h>
30 #include <gtk/gtk.h>
31 #include <libxml/parser.h>
32 #include <libxml/xinclude.h>
33 #include <libxml/xpath.h>
35 #include "yelp-help-list.h"
36 #include "yelp-settings.h"
38 typedef struct _HelpListEntry HelpListEntry;
40 static void yelp_help_list_class_init (YelpHelpListClass *klass);
41 static void yelp_help_list_init (YelpHelpList *list);
42 static void yelp_help_list_dispose (GObject *object);
43 static void yelp_help_list_finalize (GObject *object);
45 static gboolean help_list_request_page (YelpDocument *document,
46 const gchar *page_id,
47 GCancellable *cancellable,
48 YelpDocumentCallback callback,
49 gpointer user_data);
50 static void help_list_think (YelpHelpList *list);
51 static void help_list_handle_page (YelpHelpList *list,
52 const gchar *page_id);
53 static void help_list_process_docbook (YelpHelpList *list,
54 HelpListEntry *entry);
55 static void help_list_process_mallard (YelpHelpList *list,
56 HelpListEntry *entry);
58 static const char*const known_vendor_prefixes[] = { "gnome",
59 "fedora",
60 "mozilla",
61 NULL };
63 struct _HelpListEntry
65 gchar *id;
66 gchar *title;
67 gchar *desc;
68 gchar *icon;
70 gchar *filename;
71 YelpUriDocumentType type;
73 static void
74 help_list_entry_free (HelpListEntry *entry)
76 g_free (entry->id);
77 g_free (entry->title);
78 g_free (entry->desc);
79 g_free (entry);
81 static gint
82 help_list_entry_cmp (HelpListEntry *a, HelpListEntry *b)
84 gchar *as, *bs;
85 as = a->title ? a->title : strchr (a->id, ':') + 1;
86 bs = b->title ? b->title : strchr (b->id, ':') + 1;
87 return g_utf8_collate (as, bs);
90 G_DEFINE_TYPE (YelpHelpList, yelp_help_list, YELP_TYPE_DOCUMENT);
91 #define GET_PRIV(object) (G_TYPE_INSTANCE_GET_PRIVATE ((object), YELP_TYPE_HELP_LIST, YelpHelpListPrivate))
93 typedef struct _YelpHelpListPrivate YelpHelpListPrivate;
94 struct _YelpHelpListPrivate {
95 GMutex *mutex;
96 GThread *thread;
98 gboolean process_running;
99 gboolean process_ran;
101 GHashTable *entries;
102 GList *all_entries;
103 GSList *pending;
105 xmlXPathCompExprPtr get_docbook_title;
106 xmlXPathCompExprPtr get_mallard_title;
107 xmlXPathCompExprPtr get_mallard_desc;
110 static void
111 yelp_help_list_class_init (YelpHelpListClass *klass)
113 GObjectClass *object_class = G_OBJECT_CLASS (klass);
114 YelpDocumentClass *document_class = YELP_DOCUMENT_CLASS (klass);
116 object_class->dispose = yelp_help_list_dispose;
117 object_class->finalize = yelp_help_list_finalize;
119 document_class->request_page = help_list_request_page;
121 g_type_class_add_private (klass, sizeof (YelpHelpListPrivate));
124 static void
125 yelp_help_list_init (YelpHelpList *list)
127 YelpHelpListPrivate *priv = GET_PRIV (list);
129 priv->mutex = g_mutex_new ();
130 priv->entries = g_hash_table_new_full (g_str_hash, g_str_equal,
131 g_free,
132 (GDestroyNotify) help_list_entry_free);
134 priv->get_docbook_title = xmlXPathCompile ("normalize-space("
135 "( /*/title | /*/db:title"
136 "| /*/articleinfo/title"
137 "| /*/bookinfo/title"
138 "| /*/db:info/db:title"
139 ")[1])");
140 priv->get_mallard_title = xmlXPathCompile ("normalize-space((/mal:page/mal:info/mal:title[@type='text'] |"
141 " /mal:page/mal:title)[1])");
142 priv->get_mallard_desc = xmlXPathCompile ("normalize-space(/mal:page/mal:info/mal:desc[1])");
144 yelp_document_set_page_id ((YelpDocument *) list, NULL, "index");
145 yelp_document_set_page_id ((YelpDocument *) list, "index", "index");
148 static void
149 yelp_help_list_dispose (GObject *object)
151 G_OBJECT_CLASS (yelp_help_list_parent_class)->dispose (object);
154 static void
155 yelp_help_list_finalize (GObject *object)
157 YelpHelpListPrivate *priv = GET_PRIV (object);
159 g_hash_table_destroy (priv->entries);
160 g_mutex_free (priv->mutex);
162 if (priv->get_docbook_title)
163 xmlXPathFreeCompExpr (priv->get_docbook_title);
164 if (priv->get_mallard_title)
165 xmlXPathFreeCompExpr (priv->get_mallard_title);
166 if (priv->get_mallard_desc)
167 xmlXPathFreeCompExpr (priv->get_mallard_desc);
169 G_OBJECT_CLASS (yelp_help_list_parent_class)->finalize (object);
172 YelpDocument *
173 yelp_help_list_new (YelpUri *uri)
175 return g_object_new (YELP_TYPE_HELP_LIST, NULL);
178 /******************************************************************************/
180 static gboolean
181 help_list_request_page (YelpDocument *document,
182 const gchar *page_id,
183 GCancellable *cancellable,
184 YelpDocumentCallback callback,
185 gpointer user_data)
187 gboolean handled;
188 YelpHelpListPrivate *priv = GET_PRIV (document);
190 if (page_id == NULL)
191 page_id = "index";
193 handled =
194 YELP_DOCUMENT_CLASS (yelp_help_list_parent_class)->request_page (document,
195 page_id,
196 cancellable,
197 callback,
198 user_data);
199 if (handled) {
200 return TRUE;
203 g_mutex_lock (priv->mutex);
204 if (priv->process_ran) {
205 help_list_handle_page ((YelpHelpList *) document, page_id);
206 return TRUE;
209 if (!priv->process_running) {
210 priv->process_running = TRUE;
211 g_object_ref (document);
212 priv->thread = g_thread_create ((GThreadFunc) help_list_think,
213 document, FALSE, NULL);
215 priv->pending = g_slist_prepend (priv->pending, g_strdup (page_id));
216 g_mutex_unlock (priv->mutex);
217 return TRUE;
220 static void
221 help_list_think (YelpHelpList *list)
223 const gchar * const *sdatadirs = g_get_system_data_dirs ();
224 const gchar * const *langs = g_get_language_names ();
225 YelpHelpListPrivate *priv = GET_PRIV (list);
226 /* The strings are still owned by GLib; we just own the array. */
227 gchar **datadirs;
228 gint datadir_i, subdir_i, lang_i;
229 GList *cur;
230 GtkIconTheme *theme;
232 datadirs = g_new0 (gchar *, g_strv_length ((gchar **) sdatadirs) + 2);
233 datadirs[0] = (gchar *) g_get_user_data_dir ();
234 for (datadir_i = 0; sdatadirs[datadir_i]; datadir_i++)
235 datadirs[datadir_i + 1] = (gchar *) sdatadirs[datadir_i];
237 for (datadir_i = 0; datadirs[datadir_i]; datadir_i++) {
238 gchar *helpdirname = g_build_filename (datadirs[datadir_i], "gnome", "help", NULL);
239 GFile *helpdir = g_file_new_for_path (helpdirname);
240 GFileEnumerator *children = g_file_enumerate_children (helpdir,
241 G_FILE_ATTRIBUTE_STANDARD_TYPE","
242 G_FILE_ATTRIBUTE_STANDARD_NAME,
243 G_FILE_QUERY_INFO_NONE,
244 NULL, NULL);
245 GFileInfo *child;
246 if (children == NULL) {
247 g_object_unref (helpdir);
248 g_free (helpdirname);
249 continue;
251 while (child = g_file_enumerator_next_file (children, NULL, NULL)) {
252 gchar *docid;
253 HelpListEntry *entry = NULL;
255 if (g_file_info_get_file_type (child) != G_FILE_TYPE_DIRECTORY)
256 continue;
258 docid = g_strconcat ("ghelp:", g_file_info_get_name (child), NULL);
259 if (g_hash_table_lookup (priv->entries, docid)) {
260 g_free (docid);
261 g_object_unref (child);
262 continue;
265 for (lang_i = 0; langs[lang_i]; lang_i++) {
266 gchar *filename, *tmp;
268 filename = g_build_filename (helpdirname,
269 g_file_info_get_name (child),
270 langs[lang_i],
271 "index.page",
272 NULL);
273 if (g_file_test (filename, G_FILE_TEST_IS_REGULAR)) {
274 entry = g_new0 (HelpListEntry, 1);
275 entry->id = g_strdup (docid);
276 entry->filename = filename;
277 entry->type = YELP_URI_DOCUMENT_TYPE_MALLARD;
278 break;
280 g_free (filename);
282 tmp = g_strdup_printf ("%s.xml", g_file_info_get_name (child));
283 filename = g_build_filename (helpdirname,
284 g_file_info_get_name (child),
285 langs[lang_i],
286 tmp,
287 NULL);
288 g_free (tmp);
289 if (g_file_test (filename, G_FILE_TEST_IS_REGULAR)) {
290 entry = g_new0 (HelpListEntry, 1);
291 entry->id = g_strdup (docid);
292 entry->filename = filename;
293 entry->type = YELP_URI_DOCUMENT_TYPE_DOCBOOK;
294 break;
296 g_free (filename);
299 if (entry != NULL) {
300 g_hash_table_insert (priv->entries, docid, entry);
301 priv->all_entries = g_list_prepend (priv->all_entries, entry);
303 else
304 g_free (docid);
305 g_object_unref (child);
307 g_object_unref (children);
308 g_object_unref (helpdir);
309 g_free (helpdirname);
311 g_free (datadirs);
313 theme = gtk_icon_theme_get_default ();
314 for (cur = priv->all_entries; cur != NULL; cur = cur->next) {
315 GDesktopAppInfo *app;
316 gchar *tmp;
317 HelpListEntry *entry = (HelpListEntry *) cur->data;
318 const gchar *entryid = strchr (entry->id, ':') + 1;
320 if (entry->type == YELP_URI_DOCUMENT_TYPE_MALLARD)
321 help_list_process_mallard (list, entry);
322 else if (entry->type == YELP_URI_DOCUMENT_TYPE_DOCBOOK)
323 help_list_process_docbook (list, entry);
325 tmp = g_strconcat (entryid, ".desktop", NULL);
326 app = g_desktop_app_info_new (tmp);
327 g_free (tmp);
329 if (app == NULL) {
330 char **prefix;
331 for (prefix = (char **) known_vendor_prefixes; *prefix; prefix++) {
332 tmp = g_strconcat (*prefix, "-", entryid, ".desktop", NULL);
333 app = g_desktop_app_info_new (tmp);
334 g_free (tmp);
335 if (app)
336 break;
340 if (app != NULL) {
341 GIcon *icon = g_app_info_get_icon ((GAppInfo *) app);
342 if (icon != NULL) {
343 GtkIconInfo *info = gtk_icon_theme_lookup_by_gicon (theme,
344 icon, 22,
345 GTK_ICON_LOOKUP_NO_SVG);
346 if (info != NULL) {
347 const gchar *iconfile = gtk_icon_info_get_filename (info);
348 if (iconfile)
349 entry->icon = g_filename_to_uri (iconfile, NULL, NULL);
350 gtk_icon_info_free (info);
353 g_object_unref (app);
357 g_mutex_lock (priv->mutex);
358 priv->process_running = FALSE;
359 priv->process_ran = TRUE;
360 while (priv->pending) {
361 gchar *page_id = (gchar *) priv->pending->data;
362 help_list_handle_page (list, page_id);
363 g_free (page_id);
364 priv->pending = g_slist_delete_link (priv->pending, priv->pending);
366 g_mutex_unlock (priv->mutex);
368 g_object_unref (list);
371 /* This function expects to be called inside a locked mutex */
372 static void
373 help_list_handle_page (YelpHelpList *list,
374 const gchar *page_id)
376 gchar **colors, *tmp;
377 GList *cur;
378 YelpHelpListPrivate *priv = GET_PRIV (list);
379 GtkTextDirection direction = gtk_widget_get_default_direction ();
380 GString *string = g_string_new
381 ("<html><head><style type='text/css'>\n"
382 "html { height: 100%; }\n"
383 "body { margin: 0; padding: 0; max-width: 100%;");
384 colors = yelp_settings_get_colors (yelp_settings_get_default ());
386 tmp = g_markup_printf_escaped (" background-color: %s; color: %s;"
387 " direction: %s; }\n",
388 colors[YELP_SETTINGS_COLOR_BASE],
389 colors[YELP_SETTINGS_COLOR_TEXT],
390 (direction == GTK_TEXT_DIR_RTL) ? "rtl" : "ltr");
391 g_string_append (string, tmp);
392 g_free (tmp);
394 g_string_append (string,
395 "div.body { margin: 0 12px 0 12px; padding: 0;"
396 " max-width: 60em; min-height: 20em; }\n"
397 "div.header { max-width: 100%; width: 100%;"
398 " padding: 0; margin: 0 0 1em 0; }\n"
399 "div.footer { max-width: 60em; }\n"
400 "div.sect { margin-top: 1.72em; }\n"
401 "div.trails { margin: 0; padding: 0.2em 12px 0 12px;");
403 tmp = g_markup_printf_escaped (" background-color: %s;"
404 " border-bottom: solid 1px %s; }\n",
405 colors[YELP_SETTINGS_COLOR_GRAY_BASE],
406 colors[YELP_SETTINGS_COLOR_GRAY_BORDER]);
407 g_string_append (string, tmp);
408 g_free (tmp);
410 g_string_append (string,
411 "div.trail { margin: 0 1em 0.2em 1em; padding: 0; text-indent: -1em;");
413 tmp = g_markup_printf_escaped (" color: %s; }\n",
414 colors[YELP_SETTINGS_COLOR_TEXT_LIGHT]);
415 g_string_append (string, tmp);
416 g_free (tmp);
418 g_string_append (string,
419 "a.trail { white-space: nowrap; }\n"
420 "div.hgroup { margin: 0 0 0.5em 0;");
422 tmp = g_markup_printf_escaped (" color: %s;"
423 " border-bottom: solid 1px %s; }\n",
424 colors[YELP_SETTINGS_COLOR_TEXT_LIGHT],
425 colors[YELP_SETTINGS_COLOR_GRAY_BORDER]);
426 g_string_append (string, tmp);
427 g_free (tmp);
429 tmp = g_markup_printf_escaped ("div.title { margin: 0 0 0.2em 0; font-weight: bold; color: %s; }\n"
430 "div.desc { margin: 0 0 0.2em 0; }\n"
431 "div.linkdiv div.inner { padding-%s: 30px; min-height: 24px;"
432 " background-position: top %s; background-repeat: no-repeat;"
433 " -webkit-background-size: 22px 22px; }\n"
434 "div.linkdiv div.title {font-size: 1em; color: inherit; }\n"
435 "div.linkdiv div.desc { color: %s; }\n"
436 "div.linkdiv { margin: 0; padding: 0.5em; }\n"
437 "a:hover div.linkdiv {"
438 " text-decoration: none;"
439 " outline: solid 1px %s;"
440 " background: -webkit-gradient(linear, left top, left 80,"
441 " from(%s), to(%s)); }\n",
442 colors[YELP_SETTINGS_COLOR_TEXT_LIGHT],
443 ((direction == GTK_TEXT_DIR_RTL) ? "right" : "left"),
444 ((direction == GTK_TEXT_DIR_RTL) ? "right" : "left"),
445 colors[YELP_SETTINGS_COLOR_TEXT_LIGHT],
446 colors[YELP_SETTINGS_COLOR_BLUE_BASE],
447 colors[YELP_SETTINGS_COLOR_BLUE_BASE],
448 colors[YELP_SETTINGS_COLOR_BASE]);
449 g_string_append (string, tmp);
450 g_free (tmp);
452 g_string_append (string,
453 "h1, h2, h3, h4, h5, h6, h7 { margin: 0; padding: 0; font-weight: bold; }\n"
454 "h1 { font-size: 1.44em; }\n"
455 "h2 { font-size: 1.2em; }"
456 "h3.title, h4.title, h5.title, h6.title, h7.title { font-size: 1.2em; }"
457 "h3, h4, h5, h6, h7 { font-size: 1em; }"
458 "p { line-height: 1.72em; }"
459 "div, pre, p { margin: 1em 0 0 0; padding: 0; }"
460 "div:first-child, pre:first-child, p:first-child { margin-top: 0; }"
461 "div.inner, div.contents, pre.contents { margin-top: 0; }"
462 "p img { vertical-align: middle; }"
463 "a {"
464 " text-decoration: none;");
466 tmp = g_markup_printf_escaped (" color: %s; } a:visited { color: %s; }",
467 colors[YELP_SETTINGS_COLOR_LINK],
468 colors[YELP_SETTINGS_COLOR_LINK_VISITED]);
469 g_string_append (string, tmp);
470 g_free (tmp);
472 g_string_append (string,
473 "a:hover { text-decoration: underline; }\n"
474 "a img { border: none; }\n"
475 "</style>\n");
477 tmp = g_markup_printf_escaped ("<title>%s</title>",
478 _("All Help Documents"));
479 g_string_append (string, tmp);
480 g_free (tmp);
482 g_string_append (string,
483 "</head><body>"
484 "<div class='header'></div>"
485 "<div class='body'><div class='hgroup'>");
486 tmp = g_markup_printf_escaped ("<h1>%s</h1></div>\n",
487 _("All Help Documents"));
488 g_string_append (string, tmp);
489 g_free (tmp);
491 priv->all_entries = g_list_sort (priv->all_entries,
492 (GCompareFunc) help_list_entry_cmp);
493 for (cur = priv->all_entries; cur != NULL; cur = cur->next) {
494 HelpListEntry *entry = (HelpListEntry *) cur->data;
495 gchar *title = entry->title ? entry->title : (strchr (entry->id, ':') + 1);
496 gchar *desc = entry->desc ? entry->desc : "";
498 tmp = g_markup_printf_escaped ("<a href='%s'><div class='linkdiv'>",
499 entry->id);
500 g_string_append (string, tmp);
501 g_free (tmp);
503 if (entry->icon) {
504 tmp = g_markup_printf_escaped ("<div class='inner' style='background-image: url(%s);'>",
505 entry->icon);
506 g_string_append (string, tmp);
507 g_free (tmp);
509 else
510 g_string_append (string, "<div class='inner'>");
512 tmp = g_markup_printf_escaped ("<div class='title'>%s</div>"
513 "<div class='desc'>%s</div>"
514 "</div></div></a>",
515 title, desc);
516 g_string_append (string, tmp);
517 g_free (tmp);
520 g_string_append (string,
521 "</div>"
522 "<div class='footer'></div>"
523 "</body></html>");
525 yelp_document_give_contents (YELP_DOCUMENT (list), page_id,
526 string->str,
527 "text/html");
528 g_strfreev (colors);
529 g_string_free (string, FALSE);
530 yelp_document_signal (YELP_DOCUMENT (list), page_id,
531 YELP_DOCUMENT_SIGNAL_CONTENTS, NULL);
535 static void
536 help_list_process_docbook (YelpHelpList *list,
537 HelpListEntry *entry)
539 xmlParserCtxtPtr parserCtxt;
540 xmlDocPtr xmldoc;
541 xmlXPathContextPtr xpath;
542 xmlXPathObjectPtr obj = NULL;
543 YelpHelpListPrivate *priv = GET_PRIV (list);
545 parserCtxt = xmlNewParserCtxt ();
546 xmldoc = xmlCtxtReadFile (parserCtxt,
547 (const char *) entry->filename, NULL,
548 XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
549 XML_PARSE_NOENT | XML_PARSE_NONET );
550 xmlFreeParserCtxt (parserCtxt);
551 if (xmldoc == NULL)
552 return;
554 xmlXIncludeProcessFlags (xmldoc,
555 XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
556 XML_PARSE_NOENT | XML_PARSE_NONET );
558 xpath = xmlXPathNewContext (xmldoc);
559 xmlXPathRegisterNs (xpath, BAD_CAST "db",
560 BAD_CAST "http://docbook.org/ns/docbook");
561 obj = xmlXPathCompiledEval (priv->get_docbook_title, xpath);
562 if (obj) {
563 if (obj->stringval)
564 entry->title = g_strdup (obj->stringval);
565 xmlXPathFreeObject (obj);
568 if (xmldoc)
569 xmlFreeDoc (xmldoc);
570 if (xpath)
571 xmlXPathFreeContext (xpath);
574 static void
575 help_list_process_mallard (YelpHelpList *list,
576 HelpListEntry *entry)
578 xmlParserCtxtPtr parserCtxt;
579 xmlDocPtr xmldoc;
580 xmlXPathContextPtr xpath;
581 xmlXPathObjectPtr obj = NULL;
582 YelpHelpListPrivate *priv = GET_PRIV (list);
584 parserCtxt = xmlNewParserCtxt ();
585 xmldoc = xmlCtxtReadFile (parserCtxt,
586 (const char *) entry->filename, NULL,
587 XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
588 XML_PARSE_NOENT | XML_PARSE_NONET );
589 xmlFreeParserCtxt (parserCtxt);
590 if (xmldoc == NULL)
591 return;
593 xmlXIncludeProcessFlags (xmldoc,
594 XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
595 XML_PARSE_NOENT | XML_PARSE_NONET );
597 xpath = xmlXPathNewContext (xmldoc);
598 xmlXPathRegisterNs (xpath, BAD_CAST "mal",
599 BAD_CAST "http://projectmallard.org/1.0/");
601 obj = xmlXPathCompiledEval (priv->get_mallard_title, xpath);
602 if (obj) {
603 if (obj->stringval)
604 entry->title = g_strdup (obj->stringval);
605 xmlXPathFreeObject (obj);
608 obj = xmlXPathCompiledEval (priv->get_mallard_desc, xpath);
609 if (obj) {
610 if (obj->stringval)
611 entry->desc = g_strdup (obj->stringval);
612 xmlXPathFreeObject (obj);
615 if (xmldoc)
616 xmlFreeDoc (xmldoc);
617 if (xpath)
618 xmlXPathFreeContext (xpath);