Initial work on a help API



Hi all,

I had a project called Squawk I was working on last year.
I chatted a bit with Ryan Lortie about it at the recent
help hackfest, and decided to work on it again, but this
time directly in GLib and GTK+.

The basic idea is that GLib has a help provider API that
knows about your application's help. It knows more than
just that the help exists. It actually knows about all
the topics in your help, their URIs, titles, tags, etc.

Then there's a set of GTK+ widgets that consume this.
If you want to make a help button in a dialog, instead
of just making a button and attaching a static URI to
it, you'd make a help button and give it a tag. Then
it's up to help authors to tag topics for that button.

Then do menus the same way. We can kill off the crappy
Help->Contents item, and have the Help menu display
whatever topics help authors feel are worth promoting
to the menu. You can change widget tags at run-time to
make them reflect UI state or what the user is doing.

On top of that, what I'd like to do is add a search entry
directly into the help menu, so as you start typing, the
menu is repopulated with matching help topics.

So I've got a basic implementation, which I've attached.
There's a patch for GLib and a patch for GTK+. The GLib
API uses extension points, and the default help provider
does almost nothing. So I've got a bad proof-of-concept
module to read Mallard documents from ghelp: URIs. I've
attached that too. Build with:

gcc -shared -o libgnomehelp.so \
 `pkg-config --cflags --libs gio-2.0 libxml-2.0` \
 gnomehelp.c

Then copy to $libdir/gio/modules and run gio-querymodules.

In GTK+, under tests, there's testhelpbutton. Build it
and run it, passing a ghelp URI. ghelp:gnome-help is a
good example. The text entry is the tag. Type something
there to set the tag for the help button. Try "mouse"
or "button".

If there are no matching topics, the button is insensitive.
If there's only one matching topic, the button just links
to that topic. If there are multiple, the button becomes a
menu button. Click it to get a menu of topics.

Caveats: The gnomehelp implementation is sucky in all
sorts of ways. First, it's not even doing tags right.
The intention is that we'd have proper attached to each
page. We have markup for that. But none of the pages you
have installed have those. So in the interest of testing,
the module just treats page titles as if they were also
a list of tags.

Also, it reads all the Mallard pages files for a document.
I've talked to Ryan about ways page information could be
cached. That's the way forward. This module is only for
showing off the idea. Don't get hung up on implementation.

What I'm really interested in is comments on the GLib and
GTK+ APIs, as well as the general direction. Are people
interested in this?

Thanks,
Shaun

>From 6f51564831c7710f4216424784ea34044f0a1ad2 Mon Sep 17 00:00:00 2001
From: Shaun McCance <shaunm gnome org>
Date: Wed, 20 Apr 2011 09:09:46 -0400
Subject: [PATCH] ghelp: Initial checking of GHelp API

---
 gio/Makefile.am        |   12 ++-
 gio/ghelp.c            |  173 +++++++++++++++++++++++++++++++++
 gio/ghelp.h            |  107 ++++++++++++++++++++
 gio/gio.h              |    1 +
 gio/giomodule.c        |    5 +
 gio/giotypes.h         |    2 +
 gio/gnullhelp.c        |  251 ++++++++++++++++++++++++++++++++++++++++++++++++
 gio/tests/.gitignore   |    1 +
 gio/tests/Makefile.am  |    4 +
 gio/tests/ghelp-list.c |   65 +++++++++++++
 10 files changed, 620 insertions(+), 1 deletions(-)
 create mode 100644 gio/ghelp.c
 create mode 100644 gio/ghelp.h
 create mode 100644 gio/gnullhelp.c
 create mode 100644 gio/tests/ghelp-list.c

diff --git a/gio/Makefile.am b/gio/Makefile.am
index e2fcd7e..5a30e28 100644
--- a/gio/Makefile.am
+++ b/gio/Makefile.am
@@ -150,6 +150,14 @@ application_sources = \
 	gapplicationimpl-dbus.c			\
 	gapplication.c
 
+ghelp_headers = \
+	ghelp.h
+
+ghelp_sources = \
+	ghelp.h				\
+	ghelp.c				\
+	gnullhelp.c
+
 local_sources = \
 	glocaldirectorymonitor.c 	\
 	glocaldirectorymonitor.h 	\
@@ -400,6 +408,7 @@ libgio_2_0_la_SOURCES =		\
 	$(win32_sources) 	\
 	$(application_sources) 	\
 	$(settings_sources) 	\
+	$(ghelp_sources)	\
 	$(gdbus_sources) 	\
 	$(local_sources) 	\
 	$(marshal_sources) 	\
@@ -546,6 +555,7 @@ gio_headers =			\
 	gzlibdecompressor.h	\
 	$(application_headers)	\
 	$(settings_headers)	\
+	$(ghelp_headers)	\
 	$(gdbus_headers)	\
 	$(NULL)
 
@@ -652,7 +662,7 @@ dist-hook: $(BUILT_EXTRA_DIST) ../build/win32/vs9/gio.vcproj ../build/win32/vs10
 	done | sort -u >libgio.sourcefiles
 	$(CPP) -P - <$(top_srcdir)/build/win32/vs9/gio.vcprojin >$@
 	rm libgio.sourcefiles
-	
+
 ../build/win32/vs10/gio.vcxproj: $(top_srcdir)/build/win32/vs10/gio.vcxprojin
 	for F in `echo $(libgio_2_0_la_SOURCES) $(win32_actual_sources) $(win32_actual_more_sources_for_vcproj) | tr '/' '\\'`; do \
 		case $$F in \
diff --git a/gio/ghelp.c b/gio/ghelp.c
new file mode 100644
index 0000000..6f90077
--- /dev/null
+++ b/gio/ghelp.c
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library 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) any later version.
+ *
+ * This library 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 this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Shaun McCance <shaunm gnome org>
+ */
+
+#include "config.h"
+#include "ghelp.h"
+#include "giomodule-priv.h"
+#include "glibintl.h"
+
+
+/**
+ * SECTION:ghelp
+ * @short_description: FIXME
+ * @include: gio/gio.h
+ * 
+ * FIXME
+ **/
+
+typedef GHelpIface GHelpInterface;
+G_DEFINE_INTERFACE (GHelp, g_help, G_TYPE_OBJECT)
+
+typedef GHelpPagesIface GHelpPagesInterface;
+G_DEFINE_INTERFACE (GHelpPages, g_help_pages, G_TYPE_OBJECT)
+
+static void
+g_help_default_init (GHelpIface *iface)
+{
+  g_object_interface_install_property (iface,
+                                       g_param_spec_string ("uri",
+                                                            P_("URI"),
+                                                            P_("The base URI of the help source"),
+                                                            NULL,
+                                                            G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB |
+                                                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+  g_object_interface_install_property (iface,
+                                       g_param_spec_boolean ("ready",
+                                                             P_("Ready"),
+                                                             P_("Whether the help source is ready to serve pages"),
+                                                             FALSE,
+                                                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB |
+                                                             G_PARAM_READWRITE));
+  g_object_interface_install_property (iface,
+                                       g_param_spec_boolean ("searchable",
+                                                             P_("Searchable"),
+                                                             P_("If TRUE, text search is supported"),
+                                                             FALSE,
+                                                             G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB |
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+}
+
+static void
+g_help_pages_default_init (GHelpPagesIface *iface)
+{
+}
+
+
+/**
+ * g_help_new:
+ * @uri: FIXME
+ * 
+ * FIXME
+ *
+ * Returns: (transfer full): a new #GHelp object.
+ **/
+GHelp *
+g_help_new (const char *uri)
+{
+  GType extension_type;
+  static gsize backend;
+
+  g_return_val_if_fail (uri != NULL, NULL);
+
+  if (g_once_init_enter (&backend))
+    {
+      GIOExtensionPoint *point;
+      GIOExtension *extension;
+      GList *extensions;
+
+      _g_io_modules_ensure_loaded ();
+
+      point = g_io_extension_point_lookup (G_HELP_EXTENSION_POINT_NAME);
+      extension = NULL;
+
+      extensions = g_io_extension_point_get_extensions (point);
+
+      if (extensions == NULL)
+        g_error ("No GHelp implementations exist.");
+
+      extension = extensions->data;
+
+      g_once_init_leave (&backend, (gsize) extension);
+    }
+
+  extension_type = g_io_extension_get_type ((GIOExtension *) backend);
+  return g_object_new (extension_type,
+                       "uri", uri,
+                       NULL);
+}
+
+void
+g_help_restrict_to_tags (GHelp       *help,
+                         const char  *tags)
+{
+  GHelpInterface *iface = G_HELP_GET_IFACE (help);
+
+  if (iface->restrict_to_tags)
+    iface->restrict_to_tags (help, tags);
+}
+
+GHelpPages *
+g_help_list_for_tags (GHelp       *help,
+                      const char  *tags)
+{
+  GHelpInterface *iface = G_HELP_GET_IFACE (help);
+
+  if (iface->list_for_tags)
+    return iface->list_for_tags (help, tags);
+
+  /* FIXME */
+  return NULL;
+}
+
+gint
+g_help_pages_get_count (GHelpPages  *pages)
+{
+  GHelpPagesInterface *iface = G_HELP_PAGES_GET_IFACE (pages);
+
+  if (iface->get_count)
+    return iface->get_count (pages);
+
+  return 0;
+}
+
+const char *
+g_help_pages_get_uri (GHelpPages  *pages,
+                      gint         num)
+{
+  GHelpPagesInterface *iface = G_HELP_PAGES_GET_IFACE (pages);
+
+  if (iface->get_uri)
+    return iface->get_uri (pages, num);
+
+  return NULL;
+}
+
+const char *
+g_help_pages_get_title (GHelpPages  *pages,
+                        gint         num)
+{
+  GHelpPagesInterface *iface = G_HELP_PAGES_GET_IFACE (pages);
+
+  if (iface->get_title)
+    return iface->get_title (pages, num);
+
+  return NULL;
+}
diff --git a/gio/ghelp.h b/gio/ghelp.h
new file mode 100644
index 0000000..8c5dd74
--- /dev/null
+++ b/gio/ghelp.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library 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) any later version.
+ *
+ * This library 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 this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Shaun McCance <shaunm gnome org>
+ */
+
+#if !defined (__GIO_GIO_H_INSIDE__) && !defined (GIO_COMPILATION)
+#error "Only <gio/gio.h> can be included directly."
+#endif
+
+#ifndef __G_HELP_H__
+#define __G_HELP_H__
+
+#include <gio/giotypes.h>
+
+G_BEGIN_DECLS
+
+#define G_TYPE_HELP            (g_help_get_type ())
+#define G_HELP(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_HELP, GHelp))
+#define G_IS_HELP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), G_TYPE_HELP))
+#define G_HELP_GET_IFACE(obj)  (G_TYPE_INSTANCE_GET_INTERFACE ((obj), G_TYPE_HELP, GHelpIface))
+
+#define G_TYPE_HELP_PAGES            (g_help_pages_get_type ())
+#define G_HELP_PAGES(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), G_TYPE_HELP_PAGES, GHelpPages))
+#define G_IS_HELP_PAGES(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), G_TYPE_HELP_PAGES))
+#define G_HELP_PAGES_GET_IFACE(obj)  (G_TYPE_INSTANCE_GET_INTERFACE ((obj), G_TYPE_HELP_PAGES, GHelpPagesIface))
+
+
+/**
+ * G_HELP_EXTENSION_POINT_NAME:
+ *
+ * Extension point for #GHelp functionality.
+ **/
+#define G_HELP_EXTENSION_POINT_NAME "ghelp"
+
+
+/**
+ * GHelpIface:
+ * @g_iface: The parent interface.
+ *
+ * An interface for help providers.
+ **/
+typedef struct _GHelpIface GHelpIface;
+struct _GHelpIface
+{
+  GTypeInterface g_iface;
+
+  /* Virtual Table */
+
+  void                (* restrict_to_tags)                 (GHelp         *help,
+                                                            const char    *tags);
+  GHelpPages *        (* list_for_tags)                    (GHelp         *help,
+                                                            const char    *tags);
+};
+
+/**
+ * GHelpPagesIface:
+ * @g_iface: The parent interface.
+ *
+ * FIXME
+ **/
+typedef struct _GHelpPagesIface GHelpPagesIface;
+struct _GHelpPagesIface
+{
+  GTypeInterface g_iface;
+
+  /* Virtual Table */
+  gint                (* get_count)                        (GHelpPages    *pages);
+  const char *        (* get_uri)                          (GHelpPages    *pages,
+                                                            gint           num);
+  const char *        (* get_title)                        (GHelpPages    *pages,
+                                                            gint           num);
+};
+
+GType                    g_help_get_type                   (void) G_GNUC_CONST;
+GType                    g_help_pages_get_type             (void) G_GNUC_CONST;
+
+GHelp *                  g_help_new                        (const char    *uri);
+void                     g_help_restrict_to_tags           (GHelp         *help,
+                                                            const char    *tags);
+GHelpPages *             g_help_list_for_tags              (GHelp         *help,
+                                                            const char    *tags);
+
+gint                     g_help_pages_get_count            (GHelpPages    *pages);
+const char *             g_help_pages_get_uri              (GHelpPages    *pages,
+                                                            gint           num);
+const char *             g_help_pages_get_title            (GHelpPages    *pages,
+                                                            gint           num);
+
+G_END_DECLS
+
+#endif /* __G_HELP_H__ */
diff --git a/gio/gio.h b/gio/gio.h
index 288da44..e5d5255 100644
--- a/gio/gio.h
+++ b/gio/gio.h
@@ -73,6 +73,7 @@
 #include <gio/gfileoutputstream.h>
 #include <gio/gfilterinputstream.h>
 #include <gio/gfilteroutputstream.h>
+#include <gio/ghelp.h>
 #include <gio/gicon.h>
 #include <gio/ginetaddress.h>
 #include <gio/ginetsocketaddress.h>
diff --git a/gio/giomodule.c b/gio/giomodule.c
index 00e8c48..ff7d4ff 100644
--- a/gio/giomodule.c
+++ b/gio/giomodule.c
@@ -24,6 +24,7 @@
 
 #include <string.h>
 
+#include "ghelp.h"
 #include "giomodule.h"
 #include "giomodule-priv.h"
 #include "glocalfilemonitor.h"
@@ -553,6 +554,9 @@ _g_io_modules_ensure_extension_points_registered (void)
       ep = g_io_extension_point_register ("gsettings-backend");
       g_io_extension_point_set_required_type (ep, G_TYPE_OBJECT);
 
+      ep = g_io_extension_point_register (G_HELP_EXTENSION_POINT_NAME);
+      g_io_extension_point_set_required_type (ep, G_TYPE_OBJECT);
+
       ep = g_io_extension_point_register (G_PROXY_RESOLVER_EXTENSION_POINT_NAME);
       g_io_extension_point_set_required_type (ep, G_TYPE_PROXY_RESOLVER);
 
@@ -600,6 +604,7 @@ _g_io_modules_ensure_loaded (void)
       /* Initialize types from built-in "modules" */
       g_null_settings_backend_get_type ();
       g_memory_settings_backend_get_type ();
+      g_null_help_get_type ();
 #if defined(HAVE_SYS_INOTIFY_H) || defined(HAVE_LINUX_INOTIFY_H)
       _g_inotify_directory_monitor_get_type ();
       _g_inotify_file_monitor_get_type ();
diff --git a/gio/giotypes.h b/gio/giotypes.h
index 1c35083..0a3f372 100644
--- a/gio/giotypes.h
+++ b/gio/giotypes.h
@@ -56,6 +56,8 @@ typedef struct _GApplicationCommandLine       GApplicationCommandLine;
 typedef struct _GSettingsBackend              GSettingsBackend;
 typedef struct _GSettings                     GSettings;
 typedef struct _GPermission                   GPermission;
+typedef struct _GHelp                         GHelp;
+typedef struct _GHelpPages                    GHelpPages;
 
 /**
  * GDrive:
diff --git a/gio/gnullhelp.c b/gio/gnullhelp.c
new file mode 100644
index 0000000..e04d767
--- /dev/null
+++ b/gio/gnullhelp.c
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library 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) any later version.
+ *
+ * This library 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 this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Shaun McCance <shaunm gnome org>
+ */
+
+#include "config.h"
+
+#include "ghelp.h"
+#include "giomodule-priv.h"
+#include "glibintl.h"
+
+#define G_TYPE_NULL_HELP          (g_null_help_get_type ())
+#define G_NULL_HELP(obj)          (G_TYPE_CHECK_INSTANCE_CAST((obj), G_TYPE_NULL_HELP, GNullHelp))
+
+#define G_TYPE_NULL_HELP_PAGES    (g_null_help_pages_get_type ())
+#define G_NULL_HELP_PAGES(obj)    (G_TYPE_CHECK_INSTANCE_CAST((obj), G_TYPE_NULL_HELP_PAGES, GNullHelpPages))
+
+typedef struct _GNullHelp           GNullHelp;
+typedef struct _GNullHelpClass      GNullHelpClass;
+typedef struct _GNullHelpPages      GNullHelpPages;
+typedef struct _GNullHelpPagesClass GNullHelpPagesClass;
+
+static void          g_null_help_iface_init           (GHelpIface      *iface);
+static void          g_null_help_finalize             (GObject         *object);
+static void          g_null_help_get_property         (GObject         *object,
+                                                       guint            prop_id,
+                                                       GValue          *value,
+                                                       GParamSpec      *pspec);
+static void          g_null_help_set_property         (GObject         *object,
+                                                       guint            prop_id,
+                                                       const GValue    *value,
+                                                       GParamSpec      *pspec);
+static GHelpPages *  g_null_help_list_for_tags        (GHelp           *help,
+                                                       const char      *tags);
+
+static void          g_null_help_pages_iface_init     (GHelpPagesIface *iface);
+static void          g_null_help_pages_finalize       (GObject         *object);
+static gint          g_null_help_pages_get_count      (GHelpPages      *pages);
+static const char *  g_null_help_pages_get_uri        (GHelpPages      *pages,
+                                                       gint             num);
+static const char *  g_null_help_pages_get_title      (GHelpPages      *pages,
+                                                       gint             num);
+
+
+struct _GNullHelp
+{
+  GObject   parent_instance;
+  char     *uri;
+  gboolean  ready;
+};
+
+struct _GNullHelpClass
+{
+  GObjectClass parent_class;
+};
+
+struct _GNullHelpPages
+{
+  GObject  parent_instance;
+  char    *uri;
+};
+
+struct _GNullHelpPagesClass
+{
+  GObjectClass parent_class;
+};
+
+G_DEFINE_TYPE_WITH_CODE (GNullHelp, g_null_help, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_HELP, g_null_help_iface_init)
+			 _g_io_modules_ensure_extension_points_registered ();
+                         g_io_extension_point_implement (G_HELP_EXTENSION_POINT_NAME,
+                                                         g_define_type_id, "null", 0))
+G_DEFINE_TYPE_WITH_CODE (GNullHelpPages, g_null_help_pages, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_HELP_PAGES, g_null_help_pages_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HELP_URI,
+  PROP_HELP_READY,
+  PROP_HELP_SEARCHABLE
+};
+
+static void
+g_null_help_init (GNullHelp *help)
+{
+  g_object_set (help, "ready", TRUE, NULL);
+}
+
+static void
+g_null_help_class_init (GNullHelpClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->set_property = g_null_help_set_property;
+  gobject_class->get_property = g_null_help_get_property;
+  gobject_class->finalize = g_null_help_finalize;
+
+  g_object_class_override_property (gobject_class, PROP_HELP_URI, "uri");
+  g_object_class_override_property (gobject_class, PROP_HELP_READY, "ready");
+  g_object_class_override_property (gobject_class, PROP_HELP_SEARCHABLE, "searchable");
+}
+
+static void
+g_null_help_iface_init (GHelpIface *iface)
+{
+  iface->list_for_tags = g_null_help_list_for_tags;
+}
+
+static void
+g_null_help_finalize (GObject *object)
+{
+  GNullHelp *help = G_NULL_HELP (object);
+
+  g_free (help->uri);
+
+  G_OBJECT_CLASS (g_null_help_parent_class)->finalize (object);
+}
+
+static void
+g_null_help_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  GNullHelp *help = G_NULL_HELP (object);
+
+  switch (prop_id)
+    {
+    case PROP_HELP_URI:
+      g_value_set_string (value, help->uri);
+      break;
+    case PROP_HELP_READY:
+      g_value_set_boolean (value, help->ready);
+      break;
+    case PROP_HELP_SEARCHABLE:
+      g_value_set_boolean (value, FALSE);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+g_null_help_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  GNullHelp *help = G_NULL_HELP (object);
+
+  switch (prop_id)
+    {
+    case PROP_HELP_URI:
+      help->uri = g_value_dup_string (value);
+      break;
+    case PROP_HELP_READY:
+      help->ready = g_value_get_boolean (value);
+      break;
+    case PROP_HELP_SEARCHABLE:
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+g_null_help_pages_init (GNullHelpPages *pages)
+{
+}
+
+static void
+g_null_help_pages_class_init (GNullHelpPagesClass *class)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (class);
+
+  gobject_class->finalize = g_null_help_pages_finalize;
+}
+
+static void
+g_null_help_pages_iface_init (GHelpPagesIface *iface)
+{
+  iface->get_count = g_null_help_pages_get_count;
+  iface->get_uri = g_null_help_pages_get_uri;
+  iface->get_title = g_null_help_pages_get_title;
+}
+
+static void
+g_null_help_pages_finalize (GObject *object)
+{
+  GNullHelpPages *pages = G_NULL_HELP_PAGES (object);
+
+  g_free (pages->uri);
+
+  G_OBJECT_CLASS (g_null_help_pages_parent_class)->finalize (object);
+}
+
+
+static GHelpPages *
+g_null_help_list_for_tags (GHelp       *help,
+                           const char  *tags)
+{
+  GNullHelp *nhelp;
+  GNullHelpPages *results;
+
+  nhelp = G_NULL_HELP (help);
+
+  results = g_object_new (G_TYPE_NULL_HELP_PAGES, NULL);
+  results->uri = g_strdup (nhelp->uri);
+
+  return (GHelpPages *) results;
+}
+
+static gint
+g_null_help_pages_get_count (GHelpPages *pages)
+{
+  return 1;
+}
+
+static const char *
+g_null_help_pages_get_uri (GHelpPages *pages,
+                           gint        num)
+{
+  GNullHelpPages *npages = G_NULL_HELP_PAGES (pages);
+
+  return npages->uri;
+}
+
+static const char *
+g_null_help_pages_get_title (GHelpPages *pages,
+                             gint        num)
+{
+  return _("All help topics");
+}
diff --git a/gio/tests/.gitignore b/gio/tests/.gitignore
index c7aec70..417c906 100644
--- a/gio/tests/.gitignore
+++ b/gio/tests/.gitignore
@@ -51,6 +51,7 @@ gdbus-threading
 g-file
 g-file-info
 g-icon
+ghelp-list
 gschemas.compiled
 gsettings
 gsettings.store
diff --git a/gio/tests/Makefile.am b/gio/tests/Makefile.am
index 46b0efa..0ab9c83 100644
--- a/gio/tests/Makefile.am
+++ b/gio/tests/Makefile.am
@@ -92,6 +92,7 @@ SAMPLE_PROGS = 				\
 	gapplication-example-cmdline2	\
 	gapplication-example-cmdline3	\
 	gapplication-example-actions	\
+	ghelp-list			\
 	$(NULL)
 
 
@@ -335,6 +336,9 @@ gapplication_example_cmdline3_LDADD   = $(progs_ldadd)
 gapplication_example_actions_SOURCES = gapplication-example-actions.c
 gapplication_example_actions_LDADD   = $(progs_ldadd)
 
+ghelp_list_SOURCES = ghelp-list.c
+ghelp_list_LDADD   = $(progs_ldadd)
+
 schema_tests = \
 	schema-tests/array-default-not-in-choices.gschema.xml		\
 	schema-tests/bad-choice.gschema.xml				\
diff --git a/gio/tests/ghelp-list.c b/gio/tests/ghelp-list.c
new file mode 100644
index 0000000..389b944
--- /dev/null
+++ b/gio/tests/ghelp-list.c
@@ -0,0 +1,65 @@
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+static gboolean running;
+static GMainLoop *main_loop;
+static char *tags;
+
+static void
+help_ready (GHelp *help)
+{
+  GHelpPages *pages;
+  gint i, cnt;
+
+  pages = g_help_list_for_tags (help, tags);
+  cnt = g_help_pages_get_count (pages);
+
+  for (i = 0; i < cnt; i++)
+    g_print ("%s - %s\n",
+             g_help_pages_get_uri (pages, i),
+             g_help_pages_get_title (pages, i));
+
+  g_object_unref (pages);
+  g_object_unref (help);
+
+  if (running)
+    g_main_loop_quit (main_loop);
+}
+
+int 
+main (int argc, char *argv[])
+{
+  GHelp *help;
+  gboolean ready;
+
+  g_type_init ();
+  running = FALSE;
+
+  if (argc < 2)
+    {
+      g_print ("Usage: %s URI [TAGS]\n", argv[0]);
+      return 0;
+    }
+
+  help = g_help_new (argv[1]);
+  if (argc >= 3)
+    tags = argv[2];
+  else
+    tags = "";
+
+  g_object_get (help, "ready", &ready, NULL);
+  if (ready)
+    {
+      help_ready (help);
+    }
+  else
+    {
+      g_signal_connect (help, "notify::ready",
+                        G_CALLBACK (help_ready), NULL);
+      running = TRUE;
+      g_main_loop_run (main_loop);
+    }
+
+  return 0;
+}
-- 
1.7.3.2

>From af256e62d4c17ff1e6618d57e24648207cdf7540 Mon Sep 17 00:00:00 2001
From: Shaun McCance <shaunm gnome org>
Date: Thu, 21 Apr 2011 15:53:59 -0400
Subject: [PATCH] gtkhelpbutton: Initial work on GtkHelpButton

---
 gtk/Makefile.am        |    2 +
 gtk/gtk.h              |    1 +
 gtk/gtkhelpbutton.c    |  401 ++++++++++++++++++++++++++++++++++++++++++++++++
 gtk/gtkhelpbutton.h    |   69 ++++++++
 tests/Makefile.am      |    5 +
 tests/testhelpbutton.c |   74 +++++++++
 6 files changed, 552 insertions(+), 0 deletions(-)
 create mode 100644 gtk/gtkhelpbutton.c
 create mode 100644 gtk/gtkhelpbutton.h
 create mode 100644 tests/testhelpbutton.c

diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index efe9315..a24902d 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -221,6 +221,7 @@ gtk_public_h_sources = 		\
 	gtkhandlebox.h		\
 	gtkhbbox.h		\
 	gtkhbox.h		\
+	gtkhelpbutton.h		\
 	gtkhpaned.h		\
 	gtkhscale.h		\
 	gtkhscrollbar.h		\
@@ -539,6 +540,7 @@ gtk_base_c_sources = 		\
 	gtkhandlebox.c		\
 	gtkhbbox.c		\
 	gtkhbox.c		\
+	gtkhelpbutton.c		\
 	gtkhpaned.c		\
 	gtkhscale.c		\
 	gtkhscrollbar.c		\
diff --git a/gtk/gtk.h b/gtk/gtk.h
index 0a72d24..605d1f0 100644
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
@@ -107,6 +107,7 @@
 #include <gtk/gtkhandlebox.h>
 #include <gtk/gtkhbbox.h>
 #include <gtk/gtkhbox.h>
+#include <gtk/gtkhelpbutton.h>
 #include <gtk/gtkhpaned.h>
 #include <gtk/gtkhscale.h>
 #include <gtk/gtkhscrollbar.h>
diff --git a/gtk/gtkhelpbutton.c b/gtk/gtkhelpbutton.c
new file mode 100644
index 0000000..4cd4d94
--- /dev/null
+++ b/gtk/gtkhelpbutton.c
@@ -0,0 +1,401 @@
+/*
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library 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 licence, or (at your option) any later version.
+ *
+ * This library 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 this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Shaun McCance <shaunm gnome org>
+ */
+
+#include "config.h"
+
+#ifndef _WIN32
+#define _GNU_SOURCE
+#endif
+
+#include "gtkarrow.h"
+#include "gtkbox.h"
+#include "gtkhelpbutton.h"
+#include "gtkimage.h"
+#include "gtkintl.h"
+#include "gtkmenu.h"
+#include "gtkmenuitem.h"
+#include "gtkmenushell.h"
+#include "gtkprivate.h"
+#include "gtkseparator.h"
+#include "gtkshow.h"
+
+/**
+ * SECTION:gtkhelpbutton
+ * @Short_description: FIXME
+ * @Title: GtkHelpButton
+ *
+ * FIXME
+ */
+
+enum
+{
+  PROP_0,
+  PROP_HELP,
+  PROP_TAG
+};
+
+struct _GtkHelpButtonPrivate
+{
+  GHelp      *help;
+  gchar      *tag;
+  GHelpPages *pages;
+
+  /* do not free */
+  GtkWidget *icon;
+  GtkWidget *sep;
+  GtkWidget *arrow;
+};
+
+static void        gtk_help_button_constructed       (GObject             *object);
+static void        gtk_help_button_dispose           (GObject             *object);
+static void        gtk_help_button_finalize          (GObject             *object);
+static void        gtk_help_button_set_property      (GObject             *object,
+                                                      guint                prop_id,
+                                                      const GValue        *value,
+                                                      GParamSpec          *pspec);
+static void        gtk_help_button_get_property      (GObject             *object,
+                                                      guint                prop_id,
+                                                      GValue              *value,
+                                                      GParamSpec          *pspec);
+static void        gtk_help_button_update_content    (GtkHelpButton       *button);
+static gboolean    gtk_help_button_press             (GtkWidget           *widget,
+                                                      GdkEventButton      *event);
+static gboolean    gtk_help_button_release           (GtkWidget           *widget,
+                                                      GdkEventButton      *event);
+static void        gtk_help_button_set_tag_private   (GtkHelpButton       *button,
+                                                      const gchar         *tag);
+
+G_DEFINE_TYPE (GtkHelpButton, gtk_help_button, GTK_TYPE_BUTTON)
+
+static void
+gtk_help_button_class_init (GtkHelpButtonClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  g_type_class_add_private (klass, sizeof (GtkHelpButtonPrivate));
+
+  gobject_class->constructed = gtk_help_button_constructed;
+  gobject_class->dispose = gtk_help_button_dispose;
+  gobject_class->finalize = gtk_help_button_finalize;
+  gobject_class->set_property = gtk_help_button_set_property;
+  gobject_class->get_property = gtk_help_button_get_property;
+
+  widget_class->button_press_event = gtk_help_button_press;
+  widget_class->button_release_event = gtk_help_button_release;
+
+  /**
+   * GtkHelpButton:help:
+   *
+   * A #GHelp object to get pages from.
+   **/
+  g_object_class_install_property (gobject_class,
+                                   PROP_HELP,
+                                   g_param_spec_object ("help",
+                                                        P_("Help"),
+                                                        P_("A GHelp object to get pages from"),
+                                                        G_TYPE_HELP,
+                                                        GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+  /**
+   * GtkHelpButton:tag:
+   *
+   * Tag to match against help pages. Every page in a #GHelp object has a set
+   * of associated tags. The #GtkHelpButton will show all matching pages from
+   * its #GHelp object, where a page matches if any of its tags is the same
+   * as the button's tag.
+   **/
+  g_object_class_install_property (gobject_class,
+                                   PROP_TAG,
+                                   g_param_spec_string ("tag",
+                                                        P_("Tag"),
+                                                        P_("The tag to match help pages"),
+                                                        "",
+                                                        GTK_PARAM_READWRITE));
+}
+
+static void
+gtk_help_button_init (GtkHelpButton *button)
+{
+  button->priv = G_TYPE_INSTANCE_GET_PRIVATE (button,
+                                              GTK_TYPE_HELP_BUTTON,
+                                              GtkHelpButtonPrivate);
+}
+
+static void
+gtk_help_button_set_property (GObject       *object,
+                              guint          prop_id,
+                              const GValue  *value,
+                              GParamSpec    *pspec)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_HELP:
+      button->priv->help = g_value_dup_object (value);
+      break;
+    case PROP_TAG:
+      gtk_help_button_set_tag_private (button, g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_help_button_get_property (GObject     *object,
+                              guint        prop_id,
+                              GValue      *value,
+                              GParamSpec  *pspec)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_HELP:
+      g_value_set_object (value, button->priv->help);
+      break;
+    case PROP_TAG:
+      g_value_set_string (value, button->priv->tag);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_help_button_constructed (GObject *object)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (object);
+  GtkWidget *box;
+
+  gtk_widget_push_composite_child ();
+
+  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+  gtk_container_add (GTK_CONTAINER (button), box);
+
+  button->priv->icon = gtk_image_new_from_icon_name ("help-browser", GTK_ICON_SIZE_BUTTON);
+  gtk_box_pack_start (GTK_BOX (box), button->priv->icon, FALSE, FALSE, 0);
+
+  button->priv->sep = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
+  gtk_widget_set_no_show_all (button->priv->sep, TRUE);
+  gtk_box_pack_start (GTK_BOX (box), button->priv->sep, FALSE, FALSE, 0);
+
+  button->priv->arrow = gtk_arrow_new (GTK_ARROW_DOWN, GTK_SHADOW_NONE);
+  gtk_widget_set_no_show_all (button->priv->arrow, TRUE);
+  gtk_box_pack_start (GTK_BOX (box), button->priv->arrow, FALSE, FALSE, 0);
+
+  gtk_widget_show_all (box);
+
+  gtk_widget_pop_composite_child ();
+
+  gtk_help_button_update_content (button);
+}
+
+static void
+gtk_help_button_dispose (GObject *object)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (object);
+
+  if (button->priv->help)
+    {
+      g_object_unref (button->priv->help);
+      button->priv->help = NULL;
+    }
+
+  if (button->priv->pages)
+    {
+      g_object_unref (button->priv->pages);
+      button->priv->pages = NULL;
+    }
+
+  G_OBJECT_CLASS (gtk_help_button_parent_class)->dispose (object);
+}
+
+static void
+gtk_help_button_finalize (GObject *object)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (object);
+
+  if (button->priv->tag)
+    {
+      g_free (button->priv->tag);
+      button->priv->tag = NULL;
+    }
+
+  G_OBJECT_CLASS (gtk_help_button_parent_class)->finalize (object);
+}
+
+/**
+ * gtk_help_button_new:
+ * @help: a #GHelp object
+ * @tag: the tag to match help pages
+ *
+ * Creates a #GtkHelpButton, matching pages from @help with the tag @tag.
+ *
+ * Return value: a new #GtkHelpButton
+ */
+GtkWidget *
+gtk_help_button_new (GHelp       *help,
+                     const gchar *tag)
+{
+  GtkWidget *button;
+
+  button = g_object_new (GTK_TYPE_HELP_BUTTON,
+                         "help", help,
+                         "tag", tag,
+                         NULL);
+
+  return button;
+}
+
+static void
+help_button_set_insensitive (GtkHelpButton *button)
+{
+  gtk_widget_hide (button->priv->sep);
+  gtk_widget_hide (button->priv->arrow);
+  gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
+}
+
+static void
+gtk_help_button_update_content (GtkHelpButton *button)
+{
+  gboolean ready;
+  gint count;
+
+  g_object_get (button->priv->help, "ready", &ready, NULL);
+
+  if (!ready || button->priv->tag == NULL || button->priv->tag[0] == '\0')
+    {
+      help_button_set_insensitive (button);
+      return;
+    }
+
+  if (button->priv->pages)
+    g_object_unref (button->priv->pages);
+  button->priv->pages = g_help_list_for_tags (button->priv->help, button->priv->tag);
+  count = g_help_pages_get_count (button->priv->pages);
+
+  if (count == 0)
+    {
+      help_button_set_insensitive (button);
+      return;
+    }
+
+  gtk_widget_set_sensitive (GTK_WIDGET (button), TRUE);
+  if (count == 1)
+    {
+      gtk_widget_hide (button->priv->sep);
+      gtk_widget_hide (button->priv->arrow);
+      gtk_widget_set_tooltip_text (GTK_WIDGET (button),
+                                   g_help_pages_get_title (button->priv->pages, 0));
+    }
+  else
+    {
+      gchar *tooltip;
+      gtk_widget_show (button->priv->sep);
+      gtk_widget_show (button->priv->arrow);
+      tooltip = g_strdup_printf (g_dngettext(GETTEXT_PACKAGE, "%i help topic", "%i help topics", count), count);
+      gtk_widget_set_tooltip_text (GTK_WIDGET (button), tooltip);
+      g_free (tooltip);
+    }
+}
+
+static gboolean
+gtk_help_button_press (GtkWidget      *widget,
+                       GdkEventButton *event)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (widget);
+  gint count;
+
+  count = g_help_pages_get_count (button->priv->pages);
+
+  if (count > 1)
+    {
+      gint i;
+      GtkWidget *menu;
+
+      menu = gtk_menu_new ();
+
+      for (i = 0; i < count; i++)
+        {
+          GtkWidget *item;
+          item = gtk_menu_item_new_with_label (g_help_pages_get_title (button->priv->pages, i));
+          gtk_menu_shell_append (GTK_MENU_SHELL (menu), item);
+          gtk_widget_show_all (item);
+        }
+
+      gtk_menu_attach_to_widget (GTK_MENU (menu), widget, NULL);
+      gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event->button, event->time);
+      /* FIXME: show menu */
+      /*
+      GError *error = NULL;
+      if (!gtk_show_uri (gtk_widget_get_screen (widget),
+                         g_help_pages_get_uri (button->priv->pages, 0),
+                         event->time, &error))
+        {
+          g_warning ("%s", error->message);
+          g_error_free (error);
+        }
+      */
+    }
+
+  return TRUE;
+}
+
+static gboolean
+gtk_help_button_release (GtkWidget      *widget,
+                         GdkEventButton *event)
+{
+  GtkHelpButton *button = GTK_HELP_BUTTON (widget);
+  gint count;
+
+  count = g_help_pages_get_count (button->priv->pages);
+
+  if (count == 1)
+    {
+      GError *error = NULL;
+      if (!gtk_show_uri (gtk_widget_get_screen (widget),
+                         g_help_pages_get_uri (button->priv->pages, 0),
+                         event->time, &error))
+        {
+          g_warning ("%s", error->message);
+          g_error_free (error);
+        }
+    }
+
+  return TRUE;
+}
+
+static void
+gtk_help_button_set_tag_private (GtkHelpButton *button,
+                                 const gchar   *tag)
+{
+  if (button->priv->tag)
+    g_free (button->priv->tag);
+
+  button->priv->tag = g_strdup (tag);
+
+  gtk_help_button_update_content (button);
+}
diff --git a/gtk/gtkhelpbutton.h b/gtk/gtkhelpbutton.h
new file mode 100644
index 0000000..7b7968c
--- /dev/null
+++ b/gtk/gtkhelpbutton.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library 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 licence, or (at your option) any later version.
+ *
+ * This library 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 this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ *
+ * Author: Shaun McCance <shaunm gnome org>
+ */
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#ifndef __GTK_HELP_BUTTON_H__
+#define __GTK_HELP_BUTTON_H__
+
+#include <gtk/gtkbutton.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_HELP_BUTTON                 (gtk_help_button_get_type ())
+#define GTK_HELP_BUTTON(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), GTK_TYPE_HELP_BUTTON, GtkHelpButton))
+#define GTK_HELP_BUTTON_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_HELP_BUTTON, GtkHelpButtonClass))
+#define GTK_IS_HELP_BUTTON(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GTK_TYPE_HELP_BUTTON))
+#define GTK_IS_HELP_BUTTON_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_HELP_BUTTON))
+#define GTK_HELP_BUTTON_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_HELP_BUTTON, GtkHelpButtonClass))
+
+typedef struct _GtkHelpButton        GtkHelpButton;
+typedef struct _GtkHelpButtonClass   GtkHelpButtonClass;
+typedef struct _GtkHelpButtonPrivate GtkHelpButtonPrivate;
+
+struct _GtkHelpButton
+{
+  GtkButton parent;
+
+  /*< private >*/
+  GtkHelpButtonPrivate *priv;
+};
+
+struct _GtkHelpButtonClass
+{
+  GtkButtonClass parent_class;
+
+  /* Padding for future expansion */
+  void (*_gtk_reserved1) (void);
+  void (*_gtk_reserved2) (void);
+  void (*_gtk_reserved3) (void);
+  void (*_gtk_reserved4) (void);
+};
+
+GType            gtk_help_button_get_type         (void) G_GNUC_CONST;
+GtkWidget *      gtk_help_button_new              (GHelp            *help,
+                                                   const gchar      *tag);
+
+G_END_DECLS
+
+#endif /* __GTK_HELP_BUTTON_H__ */
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 318bbfe..6dc8833 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -52,6 +52,7 @@ noinst_PROGRAMS =  $(TEST_PROGS)	\
 	testgrid			\
 	testgtk				\
 	testheightforwidth		\
+	testhelpbutton			\
 	testiconview			\
 	testiconview-keynav		\
 	testicontheme			\
@@ -197,6 +198,7 @@ testscrolledwindow_DEPENDENCIES = $(TEST_DEPS)
 testcellarea_DEPENDENCIES = $(TEST_DEPS)
 testtreemenu_DEPENDENCIES = $(TEST_DEPS)
 testwindows_DEPENDENCIES = $(TEST_DEPS)
+testhelpbutton_DEPENDENCIES = $(TEST_DEPS)
 testexpand_DEPENDENCIES = $(TEST_DEPS)
 testexpander_DEPENDENCIES = $(TEST_DEPS)
 testswitch_DEPENDENCIES = $(TEST_DEPS)
@@ -281,6 +283,7 @@ testscrolledwindow_LDADD = $(LDADDS)
 testcellarea_LDADD = $(LDADDS)
 testtreemenu_LDADD = $(LDADDS)
 testwindows_LDADD = $(LDADDS)
+testhelpbutton_LDADD = $(LDADDS)
 testexpand_LDADD = $(LDADDS)
 testexpander_LDADD = $(LDADDS)
 testswitch_LDADD = $(LDADDS)
@@ -424,6 +427,8 @@ testappchooserbutton_SOURCES = \
 testwindows_SOURCES = 	\
 	testwindows.c
 
+testhelpbutton_SOURCES = testhelpbutton.c
+
 testexpand_SOURCES = testexpand.c
 
 testexpander_SOURCES = testexpander.c
diff --git a/tests/testhelpbutton.c b/tests/testhelpbutton.c
new file mode 100644
index 0000000..663023e
--- /dev/null
+++ b/tests/testhelpbutton.c
@@ -0,0 +1,74 @@
+/* testhelpbutton.c
+ * Copyright (C) 2011 Shaun McCance
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Library General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Library General Public License for more details.
+ *
+ * You should have received a copy of the GNU Library General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+static void
+entry_changed (GtkEntry *entry, GtkHelpButton *button)
+{
+  g_object_set (button,
+                "tag", gtk_entry_get_text (entry),
+                NULL);
+}
+
+int
+main (int argc, char *argv[])
+{
+  GHelp *help;
+  GtkWidget *window, *box, *entry, *button;
+
+  if (argc != 2)
+    {
+      g_print ("Usage: %s URI\n", argv[0]);
+      return 0;
+    }
+
+  gtk_init (&argc, &argv);
+
+  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+  gtk_window_set_title (GTK_WINDOW (window), "Help Button");
+  g_signal_connect (window, "delete-event",
+                    G_CALLBACK (gtk_main_quit), window);
+
+  box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
+  g_object_set (box, "margin", 6, NULL);
+  gtk_container_add (GTK_CONTAINER (window), box);
+
+  entry = gtk_entry_new ();
+  gtk_box_pack_start (GTK_BOX (box), entry, FALSE, FALSE, 0);
+
+  help = g_help_new (argv[1]);
+  button = gtk_help_button_new (help, "");
+  g_object_set (button,
+                "expand", FALSE,
+                "halign", GTK_ALIGN_CENTER,
+                "valign", GTK_ALIGN_START,
+                NULL);
+  gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
+
+  g_signal_connect (entry, "changed",
+                    G_CALLBACK (entry_changed), button);
+
+  gtk_widget_show_all (window);
+
+  gtk_main ();
+
+  return 0;
+}
-- 
1.7.3.2

#include <glib-object.h>
#include <gio/gio.h>
#include <gmodule.h>

#include <libxml/parser.h>
#include <libxml/xinclude.h>
#include <libxml/xpath.h>

#define GNOME_TYPE_HELP          (gnome_help_get_type ())
#define GNOME_HELP(obj)          (G_TYPE_CHECK_INSTANCE_CAST((obj), GNOME_TYPE_HELP, GnomeHelp))

#define GNOME_TYPE_HELP_PAGES    (gnome_help_pages_get_type ())
#define GNOME_HELP_PAGES(obj)    (G_TYPE_CHECK_INSTANCE_CAST((obj), GNOME_TYPE_HELP_PAGES, GnomeHelpPages))

typedef struct _GnomeHelp           GnomeHelp;
typedef struct _GnomeHelpClass      GnomeHelpClass;
typedef struct _GnomeHelpPages      GnomeHelpPages;
typedef struct _GnomeHelpPagesClass GnomeHelpPagesClass;

struct _GnomeHelp
{
  GObject   parent;
  char     *uri;
  gboolean  ready;

  GRegex *split;
  xmlXPathCompExprPtr get_id;
  xmlXPathCompExprPtr get_title;

  GHashTable *ids;
  GHashTable *titles;
  GHashTable *tags;
};

struct _GnomeHelpClass
{
  GObjectClass parent_class;
};

struct _GnomeHelpPages
{
  GObject    parent;
  GnomeHelp *help;
  gchar     *tag;
};

struct _GnomeHelpPagesClass
{
  GObjectClass parent_class;
};

static void         gnome_help_constructed       (GObject          *object);
static void         gnome_help_finalize          (GObject          *object);
static void         gnome_help_iface_init        (GHelpIface       *iface);
static void         gnome_help_get_property      (GObject          *object,
                                                  guint             prop_id,
                                                  GValue           *value,
                                                  GParamSpec       *pspec);
static void         gnome_help_set_property      (GObject          *object,
                                                  guint             prop_id,
                                                  const GValue     *value,
                                                  GParamSpec       *pspec);
static GHelpPages * gnome_help_list_for_tags     (GHelp            *help,
                                                  const char       *tags);

static void         gnome_help_pages_dispose     (GObject          *object);
static void         gnome_help_pages_finalize    (GObject          *object);
static void         gnome_help_pages_iface_init  (GHelpPagesIface  *iface);
static gint         gnome_help_pages_get_count   (GHelpPages       *pages);
static const char * gnome_help_pages_get_uri     (GHelpPages       *pages,
                                                  gint              num);
static const char * gnome_help_pages_get_title   (GHelpPages       *pages,
                                                  gint              num);

static void         gnome_help_thread            (GnomeHelp        *help);
static void         gnome_help_read_page         (GnomeHelp        *help,
                                                  const gchar      *page);
static gboolean     gnome_help_thread_done       (GnomeHelp        *help);

G_DEFINE_DYNAMIC_TYPE_EXTENDED (GnomeHelp, gnome_help, G_TYPE_OBJECT, 0,
                                G_IMPLEMENT_INTERFACE_DYNAMIC (G_TYPE_HELP,
                                                               gnome_help_iface_init))
G_DEFINE_DYNAMIC_TYPE_EXTENDED (GnomeHelpPages, gnome_help_pages, G_TYPE_OBJECT, 0,
                                G_IMPLEMENT_INTERFACE_DYNAMIC (G_TYPE_HELP_PAGES,
                                                               gnome_help_pages_iface_init))

enum {
  PROP_0,
  PROP_HELP_URI,
  PROP_HELP_READY,
  PROP_HELP_SEARCHABLE
};

static void
ptr_array_free_all (GPtrArray *array)
{
  /* actual strings belong to help->titles */
  g_ptr_array_free (array, TRUE);
}

static void
gnome_help_init (GnomeHelp *help)
{
  help->split = g_regex_new ("\\W", 0, 0, NULL);
  help->ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  help->titles = g_hash_table_new_full (g_str_hash, g_str_equal,
                                        g_free, g_free);
  help->tags = g_hash_table_new_full (g_str_hash, g_str_equal,
                                      g_free, (GDestroyNotify) ptr_array_free_all);
}

static void
gnome_help_class_init (GnomeHelpClass *class)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (class);

  gobject_class->set_property = gnome_help_set_property;
  gobject_class->get_property = gnome_help_get_property;
  gobject_class->constructed = gnome_help_constructed;
  gobject_class->finalize = gnome_help_finalize;

  g_object_class_override_property (gobject_class, PROP_HELP_URI, "uri");
  g_object_class_override_property (gobject_class, PROP_HELP_READY, "ready");
  g_object_class_override_property (gobject_class, PROP_HELP_SEARCHABLE, "searchable");
}

static void
gnome_help_iface_init (GHelpIface *iface)
{
  iface->list_for_tags = gnome_help_list_for_tags;
}

static void
gnome_help_constructed (GObject *object)
{
  GnomeHelp *help = GNOME_HELP (object);

  help->get_id = xmlXPathCompile ("string(/mal:page/@id)");
  help->get_title = xmlXPathCompile ("normalize-space((/mal:page/mal:info/mal:title[@type='text'] |"
                                     "                 /mal:page/mal:title)[1])");

  g_object_ref (object);
  g_thread_create ((GThreadFunc) gnome_help_thread, object, FALSE, NULL);
}

static void
gnome_help_finalize (GObject *object)
{
  GnomeHelp *help = GNOME_HELP (object);

  g_free (help->uri);

  g_regex_unref (help->split);

  if (help->get_id)
    {
      xmlXPathFreeCompExpr (help->get_id);
      help->get_id = NULL;
    }
  if (help->get_title)
    {
      xmlXPathFreeCompExpr (help->get_title);
      help->get_title = NULL;
    }

  g_hash_table_destroy (help->ids);
  g_hash_table_destroy (help->titles);
  g_hash_table_destroy (help->tags);

  G_OBJECT_CLASS (gnome_help_parent_class)->finalize (object);
}

static void
gnome_help_class_finalize (GnomeHelpClass *class)
{
}

static void
gnome_help_get_property (GObject    *object,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  GnomeHelp *help = GNOME_HELP (object);

  switch (prop_id)
    {
    case PROP_HELP_URI:
      g_value_set_string (value, help->uri);
      break;
    case PROP_HELP_READY:
      g_value_set_boolean (value, help->ready);
      break;
    case PROP_HELP_SEARCHABLE:
      g_value_set_boolean (value, FALSE);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
gnome_help_set_property (GObject      *object,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  GnomeHelp *help = GNOME_HELP (object);

  switch (prop_id)
    {
    case PROP_HELP_URI:
      help->uri = g_value_dup_string (value);
      break;
    case PROP_HELP_READY:
      help->ready = g_value_get_boolean (value);
      break;
    case PROP_HELP_SEARCHABLE:
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
gnome_help_thread (GnomeHelp *help)
{
  const gchar * const *datadirs = g_get_system_data_dirs ();
  const gchar * const *langs = g_get_language_names ();
  gchar *docid; /* do not free */
  gint datadir_i, lang_i;

  if (!g_str_has_prefix (help->uri, "ghelp:"))
    goto done;

  docid = help->uri + 6;

  for (datadir_i = 0; datadirs[datadir_i]; datadir_i++)
    {
      for (lang_i = 0; langs[lang_i]; lang_i++)
        {
          GFile *gfile;
          GFileEnumerator *children;
          GFileInfo *pageinfo;
          gchar *helpdir = g_build_filename (datadirs[datadir_i],
                                             "gnome", "help",
                                             docid,
                                             langs[lang_i],
                                             NULL);
          if (!g_file_test (helpdir, G_FILE_TEST_IS_DIR))
            {
              g_free (helpdir);
              continue;
            }

          gfile = g_file_new_for_path (helpdir);
          children = g_file_enumerate_children (gfile,
                                                G_FILE_ATTRIBUTE_STANDARD_NAME,
                                                G_FILE_QUERY_INFO_NONE,
                                                NULL, NULL);
          while ((pageinfo = g_file_enumerator_next_file (children, NULL, NULL)))
            {
              gchar *filename;
              filename = g_file_info_get_attribute_as_string (pageinfo,
                                                              G_FILE_ATTRIBUTE_STANDARD_NAME);
              if (g_str_has_suffix (filename, ".page"))
                {
                  gchar *fullpath;
                  GFile *pagefile = g_file_resolve_relative_path (gfile, filename);
                  fullpath = g_file_get_path (pagefile);
                  gnome_help_read_page (help, fullpath);
                  g_free (fullpath);
                  g_object_unref (pagefile);
                }
              g_free (filename);
              g_object_unref (pageinfo);
            }

          g_free (helpdir);
          g_object_unref (gfile);
          g_object_unref (children);
        }
    }

 done:
  g_idle_add ((GSourceFunc) gnome_help_thread_done, help);
}

static void
gnome_help_read_page (GnomeHelp   *help,
                      const gchar *page)
{
  xmlParserCtxtPtr parser;
  xmlDocPtr doc;
  xmlXPathContextPtr xpath;
  xmlXPathObjectPtr obj;
  gchar *id = NULL, *title = NULL;

  parser = xmlNewParserCtxt ();
  doc = xmlCtxtReadFile (parser, (const char *) page, NULL,
                         XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
                         XML_PARSE_NOENT   | XML_PARSE_NONET   );
  if (doc == NULL)
    {
      xmlFreeParserCtxt (parser);
      return;
    }
  if (xmlXIncludeProcessFlags (doc,
                               XML_PARSE_DTDLOAD | XML_PARSE_NOCDATA |
                               XML_PARSE_NOENT   | XML_PARSE_NONET   )
      < 0)
    {
      xmlFreeParserCtxt (parser);
      return;
    }

  xpath = xmlXPathNewContext (doc);
  xmlXPathRegisterNs (xpath, BAD_CAST "mal",
                      BAD_CAST "http://projectmallard.org/1.0/";);

  obj = xmlXPathCompiledEval (help->get_id, xpath);
  if (obj)
    {
      if (obj->stringval)
        id = g_strdup (obj->stringval);
      xmlXPathFreeObject (obj);
    }

  if (id == NULL || g_hash_table_lookup (help->ids, id))
    {
      g_free (id);
      xmlXPathFreeContext (xpath);
      return;
    }

  obj = xmlXPathCompiledEval (help->get_title, xpath);
  if (obj)
    {
      if (obj->stringval)
        title = g_strdup (obj->stringval);
      xmlXPathFreeObject (obj);
    }

  if (title != NULL)
    {
      gchar **tags;
      gchar *uri;
      gint i;

      uri = g_strconcat (help->uri, "?", id, NULL);
      g_hash_table_insert (help->ids, id, id);
      g_hash_table_insert (help->titles, uri, title);

      /* This is absolutely not how this is supposed to work. Tags aren't
       * text search. There will be a mechanism of putting defined tags
       * into pages. But for testing right now, we don't have those tags,
       * so we'll just treat titles as if they're a list of tags.
       */
      tags = g_regex_split (help->split, title, 0);
      for (i = 0; tags[i]; i++)
        {
          GPtrArray *array;
          gint j;

          for (j = 0; tags[i][j] != '\0'; j++)
            tags[i][j] = g_ascii_tolower (tags[i][j]);

          array = g_hash_table_lookup (help->tags, tags[i]);
          if (array == NULL)
            {
              array = g_ptr_array_new ();
              g_hash_table_insert (help->tags, g_strdup (tags[i]), array);
            }
          g_ptr_array_add (array, uri);
        }
      g_strfreev (tags);
    }

  xmlXPathFreeContext (xpath);
}

static gboolean
gnome_help_thread_done (GnomeHelp *help)
{
  if (help->get_id)
    {
      xmlXPathFreeCompExpr (help->get_id);
      help->get_id = NULL;
    }
  if (help->get_title)
    {
      xmlXPathFreeCompExpr (help->get_title);
      help->get_title = NULL;
    }
  g_object_set (help, "ready", TRUE, NULL);
  g_object_unref (help);
  return FALSE;
}

static void
gnome_help_pages_init (GnomeHelpPages *ghelp)
{
}

static void
gnome_help_pages_class_init (GnomeHelpPagesClass *class)
{
  GObjectClass *object_class;

  object_class = (GObjectClass *) class;
  object_class->dispose = gnome_help_pages_dispose;
  object_class->finalize = gnome_help_pages_finalize;
}

static void
gnome_help_pages_iface_init (GHelpPagesIface *iface)
{
  iface->get_count = gnome_help_pages_get_count;
  iface->get_uri = gnome_help_pages_get_uri;
  iface->get_title = gnome_help_pages_get_title;
}

static void
gnome_help_pages_dispose (GObject *object)
{
  GnomeHelpPages *pages = GNOME_HELP_PAGES (object);

  if (pages->help)
    {
      g_object_unref (pages->help);
      pages->help = NULL;
    }

  G_OBJECT_CLASS (gnome_help_pages_parent_class)->dispose (object);
}

static void
gnome_help_pages_finalize (GObject *object)
{
  GnomeHelpPages *pages = GNOME_HELP_PAGES (object);

  g_free (pages->tag);

  G_OBJECT_CLASS (gnome_help_pages_parent_class)->finalize (object);
}

static void
gnome_help_pages_class_finalize (GnomeHelpPagesClass *class)
{
}

static GHelpPages *
gnome_help_list_for_tags (GHelp      *help,
                          const char *tags)
{
  GnomeHelp *ghelp;
  GnomeHelpPages *results;

  ghelp = GNOME_HELP (help);

  results = g_object_new (GNOME_TYPE_HELP_PAGES, NULL);
  results->help = g_object_ref (help);
  results->tag = g_strdup (tags);

  return (GHelpPages *) results;
}

static gint
gnome_help_pages_get_count (GHelpPages *pages)
{
  GnomeHelpPages *gpages = GNOME_HELP_PAGES (pages);
  GPtrArray *array;

  array = g_hash_table_lookup (gpages->help->tags, gpages->tag);
  if (array == 0)
    return 0;
  return array->len;
}

static const char *
gnome_help_pages_get_uri (GHelpPages *pages,
                          gint        num)
{
  GnomeHelpPages *gpages = GNOME_HELP_PAGES (pages);
  GPtrArray *array;

  array = g_hash_table_lookup (gpages->help->tags, gpages->tag);
  if (array == 0)
    return NULL;
  return g_ptr_array_index (array, num);
}

static const char *
gnome_help_pages_get_title (GHelpPages *pages,
                            gint        num)
{
  GnomeHelpPages *gpages = GNOME_HELP_PAGES (pages);
  GPtrArray *array;

  array = g_hash_table_lookup (gpages->help->tags, gpages->tag);
  if (array == 0)
    return NULL;
  return g_hash_table_lookup (gpages->help->titles,
                              g_ptr_array_index (array, num));
}

void
g_io_module_load (GIOModule *module)
{
  g_type_module_use (G_TYPE_MODULE (module));

  gnome_help_register_type (G_TYPE_MODULE (module));
  gnome_help_pages_register_type (G_TYPE_MODULE (module));

  g_io_extension_point_implement (G_HELP_EXTENSION_POINT_NAME,
				  GNOME_TYPE_HELP,
				  "gnomehelp",
				  10);
}

void
g_io_module_unload (GIOModule   *module)
{
}

char **
g_io_module_query (void)
{
  char *eps[] = {
    G_HELP_EXTENSION_POINT_NAME,
    NULL
  };
  return g_strdupv (eps);
}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]