[devhelp] Implement DhBookListDirectory
- From: Sébastien Wilmet <swilmet src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [devhelp] Implement DhBookListDirectory
- Date: Sat, 28 Apr 2018 10:49:10 +0000 (UTC)
commit 2f16efe0641d0e85f6692503af7107ed556a60ce
Author: Sébastien Wilmet <swilmet gnome org>
Date: Fri Apr 27 13:35:40 2018 +0200
Implement DhBookListDirectory
devhelp/devhelp.h | 1 +
devhelp/dh-book-list-directory.c | 520 +++++++++++++++++++++++++++++++++++
devhelp/dh-book-list-directory.h | 61 ++++
devhelp/meson.build | 2 +
docs/reference/devhelp-docs.xml | 1 +
docs/reference/devhelp-sections.txt | 17 ++
po/POTFILES.in | 1 +
7 files changed, 603 insertions(+), 0 deletions(-)
---
diff --git a/devhelp/devhelp.h b/devhelp/devhelp.h
index a90a9f1..302197d 100644
--- a/devhelp/devhelp.h
+++ b/devhelp/devhelp.h
@@ -29,6 +29,7 @@
#include <devhelp/dh-book.h>
#include <devhelp/dh-book-list.h>
#include <devhelp/dh-book-list-builder.h>
+#include <devhelp/dh-book-list-directory.h>
#include <devhelp/dh-book-manager.h>
#include <devhelp/dh-book-tree.h>
#include <devhelp/dh-completion.h>
diff --git a/devhelp/dh-book-list-directory.c b/devhelp/dh-book-list-directory.c
new file mode 100644
index 0000000..a61c05c
--- /dev/null
+++ b/devhelp/dh-book-list-directory.c
@@ -0,0 +1,520 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * This file is part of Devhelp.
+ *
+ * Copyright (C) 2018 Sébastien Wilmet <swilmet gnome org>
+ *
+ * Devhelp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation, either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * Devhelp 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Devhelp. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "dh-book-list-directory.h"
+#include "dh-util-lib.h"
+
+/**
+ * SECTION:dh-book-list-directory
+ * @Title: DhBookListDirectory
+ * @Short_description: Subclass of #DhBookList containing the #DhBook's in one
+ * directory
+ *
+ * #DhBookListDirectory is a subclass of #DhBookList containing the #DhBook's in
+ * #DhBookListDirectory:directory. In that directory, each book must be in a
+ * direct sub-directory, with the Devhelp index file as a direct child of that
+ * sub-directory.
+ *
+ * For example if #DhBookListDirectory:directory is "/usr/share/gtk-doc/html/",
+ * and if there is an index file at
+ * "/usr/share/gtk-doc/html/glib/glib.devhelp2", #DhBookListDirectory will
+ * contain a #DhBook for that index file.
+ *
+ * Additionally the name of (1) the sub-directory and (2) the index file minus
+ * its extension, must match ("glib" in the example).
+ *
+ * See #DhBook for the list of allowed Devhelp index file extensions
+ * ("*.devhelp2" in the example).
+ *
+ * #DhBookListDirectory listens to the #DhBook #DhBook::deleted and
+ * #DhBook::updated signals, to remove the #DhBook or to re-create it. And
+ * #DhBookListDirectory contains a #GFileMonitor on the
+ * #DhBookListDirectory:directory to add new #DhBook's when they are installed.
+ * But note that those #GFileMonitor's are not guaranteed to work perfectly,
+ * recreating the #DhBookListDirectory (or restarting the application) may be
+ * needed to see all the index files after filesystem changes in
+ * #DhBookListDirectory:directory.
+ */
+
+#define NEW_POSSIBLE_BOOK_TIMEOUT_SECS 5
+
+typedef struct {
+ DhBookListDirectory *list_directory; /* unowned */
+ GFile *book_directory;
+ guint timeout_id;
+} NewPossibleBookData;
+
+struct _DhBookListDirectoryPrivate {
+ GFile *directory;
+ GFileMonitor *directory_monitor;
+
+ /* List of NewPossibleBookData* */
+ GSList *new_possible_books_data;
+};
+
+enum {
+ PROP_0,
+ PROP_DIRECTORY,
+ N_PROPERTIES
+};
+
+/* List of unowned DhBookListDirectory*. */
+static GList *instances;
+
+static GParamSpec *properties[N_PROPERTIES];
+
+G_DEFINE_TYPE_WITH_PRIVATE (DhBookListDirectory, dh_book_list_directory, DH_TYPE_BOOK_LIST)
+
+/* Prototypes */
+static gboolean create_book_from_index_file (DhBookListDirectory *list_directory,
+ GFile *index_file);
+
+static NewPossibleBookData *
+new_possible_book_data_new (DhBookListDirectory *list_directory,
+ GFile *book_directory)
+{
+ NewPossibleBookData *data;
+
+ data = g_new0 (NewPossibleBookData, 1);
+ data->list_directory = list_directory;
+ data->book_directory = g_object_ref (book_directory);
+
+ return data;
+}
+
+static void
+new_possible_book_data_free (gpointer _data)
+{
+ NewPossibleBookData *data = _data;
+
+ if (data == NULL)
+ return;
+
+ g_clear_object (&data->book_directory);
+
+ if (data->timeout_id != 0)
+ g_source_remove (data->timeout_id);
+
+ g_free (data);
+}
+
+static void
+book_deleted_cb (DhBook *book,
+ DhBookListDirectory *list_directory)
+{
+ dh_book_list_remove_book (DH_BOOK_LIST (list_directory), book);
+}
+
+static void
+book_updated_cb (DhBook *book,
+ DhBookListDirectory *list_directory)
+{
+ GFile *index_file;
+
+ /* Re-create the DhBook to parse again the index file. */
+
+ index_file = dh_book_get_index_file (book);
+ g_object_ref (index_file);
+
+ dh_book_list_remove_book (DH_BOOK_LIST (list_directory), book);
+
+ create_book_from_index_file (list_directory, index_file);
+ g_object_unref (index_file);
+}
+
+/* Returns TRUE if "successful", FALSE if the next possible index file in the
+ * book directory needs to be tried.
+ */
+static gboolean
+create_book_from_index_file (DhBookListDirectory *list_directory,
+ GFile *index_file)
+{
+ GList *books;
+ GList *l;
+ DhBook *book;
+
+ books = dh_book_list_get_books (DH_BOOK_LIST (list_directory));
+
+ /* Check if a DhBook at the same location has already been loaded. */
+ for (l = books; l != NULL; l = l->next) {
+ DhBook *cur_book = DH_BOOK (l->data);
+ GFile *cur_index_file;
+
+ cur_index_file = dh_book_get_index_file (cur_book);
+
+ if (g_file_equal (index_file, cur_index_file))
+ return TRUE;
+ }
+
+ book = dh_book_new (index_file);
+ if (book == NULL)
+ return FALSE;
+
+ /* Check if book with same ID was already loaded (we need to force
+ * unique book IDs).
+ */
+ if (g_list_find_custom (books, book, (GCompareFunc)dh_book_cmp_by_id) != NULL) {
+ g_object_unref (book);
+ return TRUE;
+ }
+
+ g_signal_connect_object (book,
+ "deleted",
+ G_CALLBACK (book_deleted_cb),
+ list_directory,
+ 0);
+
+ g_signal_connect_object (book,
+ "updated",
+ G_CALLBACK (book_updated_cb),
+ list_directory,
+ 0);
+
+ dh_book_list_add_book (DH_BOOK_LIST (list_directory), book);
+ g_object_unref (book);
+
+ return TRUE;
+}
+
+/* @book_directory is a directory containing a single book, with the index file
+ * as a direct child.
+ */
+static void
+create_book_from_book_directory (DhBookListDirectory *list_directory,
+ GFile *book_directory)
+{
+ GSList *possible_index_files;
+ GSList *l;
+
+ possible_index_files = _dh_util_get_possible_index_files (book_directory);
+
+ for (l = possible_index_files; l != NULL; l = l->next) {
+ GFile *index_file = G_FILE (l->data);
+
+ if (create_book_from_index_file (list_directory, index_file))
+ break;
+ }
+
+ g_slist_free_full (possible_index_files, g_object_unref);
+}
+
+static gboolean
+new_possible_book_timeout_cb (gpointer user_data)
+{
+ NewPossibleBookData *data = user_data;
+ DhBookListDirectoryPrivate *priv = data->list_directory->priv;
+
+ data->timeout_id = 0;
+
+ create_book_from_book_directory (data->list_directory, data->book_directory);
+
+ priv->new_possible_books_data = g_slist_remove (priv->new_possible_books_data, data);
+ new_possible_book_data_free (data);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+books_directory_changed_cb (GFileMonitor *directory_monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ DhBookListDirectory *list_directory)
+{
+ DhBookListDirectoryPrivate *priv = list_directory->priv;
+ NewPossibleBookData *data;
+
+ /* With the GFileMonitor here we only handle events for new directories
+ * created. Book deletions and updates are handled by the GFileMonitor
+ * in each DhBook object.
+ */
+ if (event_type != G_FILE_MONITOR_EVENT_CREATED)
+ return;
+
+ data = new_possible_book_data_new (list_directory, file);
+
+ /* We add a timeout of several seconds so that we give time to the whole
+ * documentation to get installed. If we don't do this, we may end up
+ * trying to add the new book when even the *.devhelp2 index file is not
+ * installed yet.
+ */
+ data->timeout_id = g_timeout_add_seconds (NEW_POSSIBLE_BOOK_TIMEOUT_SECS,
+ new_possible_book_timeout_cb,
+ data);
+
+ priv->new_possible_books_data = g_slist_prepend (priv->new_possible_books_data, data);
+}
+
+static void
+monitor_books_directory (DhBookListDirectory *list_directory)
+{
+ GError *error = NULL;
+
+ g_assert (list_directory->priv->directory_monitor == NULL);
+ list_directory->priv->directory_monitor = g_file_monitor_directory (list_directory->priv->directory,
+ G_FILE_MONITOR_NONE,
+ NULL,
+ &error);
+
+ if (error != NULL) {
+ gchar *parse_name;
+
+ parse_name = g_file_get_parse_name (list_directory->priv->directory);
+
+ g_warning ("Failed to create file monitor on directory “%s”: %s",
+ parse_name,
+ error->message);
+
+ g_free (parse_name);
+ g_clear_error (&error);
+ }
+
+ if (list_directory->priv->directory_monitor != NULL) {
+ g_signal_connect_object (list_directory->priv->directory_monitor,
+ "changed",
+ G_CALLBACK (books_directory_changed_cb),
+ list_directory,
+ 0);
+ }
+}
+
+static void
+find_books (DhBookListDirectory *list_directory)
+{
+ GFileEnumerator *enumerator;
+ GError *error = NULL;
+
+ enumerator = g_file_enumerate_children (list_directory->priv->directory,
+ G_FILE_ATTRIBUTE_STANDARD_NAME,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ &error);
+
+ if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
+ g_clear_error (&error);
+ goto out;
+ }
+
+ if (error != NULL) {
+ gchar *parse_name;
+
+ parse_name = g_file_get_parse_name (list_directory->priv->directory);
+
+ g_warning ("Error when reading directory '%s': %s",
+ parse_name,
+ error->message);
+
+ g_free (parse_name);
+ g_clear_error (&error);
+ goto out;
+ }
+
+ monitor_books_directory (list_directory);
+
+ while (TRUE) {
+ GFile *book_directory = NULL;
+
+ g_file_enumerator_iterate (enumerator, NULL, &book_directory, NULL, &error);
+
+ if (error != NULL) {
+ gchar *parse_name;
+
+ parse_name = g_file_get_parse_name (list_directory->priv->directory);
+
+ g_warning ("Error when enumerating directory '%s': %s",
+ parse_name,
+ error->message);
+
+ g_free (parse_name);
+ g_clear_error (&error);
+ break;
+ }
+
+ if (book_directory == NULL)
+ break;
+
+ create_book_from_book_directory (list_directory, book_directory);
+ }
+
+out:
+ g_clear_object (&enumerator);
+}
+
+static void
+set_directory (DhBookListDirectory *list_directory,
+ GFile *directory)
+{
+ g_assert (list_directory->priv->directory == NULL);
+ g_return_if_fail (G_IS_FILE (directory));
+
+ list_directory->priv->directory = g_object_ref (directory);
+ find_books (list_directory);
+}
+
+static void
+dh_book_list_directory_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ DhBookListDirectory *list_directory = DH_BOOK_LIST_DIRECTORY (object);
+
+ switch (prop_id) {
+ case PROP_DIRECTORY:
+ g_value_set_object (value, dh_book_list_directory_get_directory (list_directory));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+dh_book_list_directory_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ DhBookListDirectory *list_directory = DH_BOOK_LIST_DIRECTORY (object);
+
+ switch (prop_id) {
+ case PROP_DIRECTORY:
+ set_directory (list_directory, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+dh_book_list_directory_dispose (GObject *object)
+{
+ DhBookListDirectory *list_directory = DH_BOOK_LIST_DIRECTORY (object);
+
+ g_clear_object (&list_directory->priv->directory);
+ g_clear_object (&list_directory->priv->directory_monitor);
+
+ g_slist_free_full (list_directory->priv->new_possible_books_data, new_possible_book_data_free);
+ list_directory->priv->new_possible_books_data = NULL;
+
+ G_OBJECT_CLASS (dh_book_list_directory_parent_class)->dispose (object);
+}
+
+static void
+dh_book_list_directory_finalize (GObject *object)
+{
+ DhBookListDirectory *list_directory = DH_BOOK_LIST_DIRECTORY (object);
+
+ instances = g_list_remove (instances, list_directory);
+
+ G_OBJECT_CLASS (dh_book_list_directory_parent_class)->finalize (object);
+}
+
+static void
+dh_book_list_directory_class_init (DhBookListDirectoryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = dh_book_list_directory_get_property;
+ object_class->set_property = dh_book_list_directory_set_property;
+ object_class->dispose = dh_book_list_directory_dispose;
+ object_class->finalize = dh_book_list_directory_finalize;
+
+ /**
+ * DhBookListDirectory:directory:
+ *
+ * The directory, as a #GFile, containing a set of Devhelp books.
+ *
+ * Since: 3.30
+ */
+ properties[PROP_DIRECTORY] =
+ g_param_spec_object ("directory",
+ "Directory",
+ "",
+ G_TYPE_FILE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+}
+
+static void
+dh_book_list_directory_init (DhBookListDirectory *list_directory)
+{
+ list_directory->priv = dh_book_list_directory_get_instance_private (list_directory);
+
+ instances = g_list_prepend (instances, list_directory);
+}
+
+/**
+ * dh_book_list_directory_new:
+ * @directory: the #DhBookListDirectory:directory.
+ *
+ * Returns a #DhBookListDirectory for @directory.
+ *
+ * If a #DhBookListDirectory instance is still alive for @directory (according
+ * to g_file_equal()), the same instance is returned with the reference count
+ * increased by one, to avoid data duplication. If no #DhBookListDirectory
+ * instance already exists for @directory, this function returns a new instance
+ * with a reference count of one (so it's the responsibility of the caller to
+ * keep the object alive if wanted, to avoid destroying and re-creating the same
+ * #DhBookListDirectory repeatedly).
+ *
+ * Returns: (transfer full): a #DhBookListDirectory for @directory.
+ * Since: 3.30
+ */
+DhBookListDirectory *
+dh_book_list_directory_new (GFile *directory)
+{
+ GList *l;
+
+ g_return_val_if_fail (G_IS_FILE (directory), NULL);
+
+ for (l = instances; l != NULL; l = l->next) {
+ DhBookListDirectory *cur_list_directory = DH_BOOK_LIST_DIRECTORY (l->data);
+
+ if (cur_list_directory->priv->directory != NULL &&
+ g_file_equal (cur_list_directory->priv->directory, directory))
+ return g_object_ref (cur_list_directory);
+ }
+
+ return g_object_new (DH_TYPE_BOOK_LIST_DIRECTORY,
+ "directory", directory,
+ NULL);
+}
+
+/**
+ * dh_book_list_directory_get_directory:
+ * @list_directory: a #DhBookListDirectory.
+ *
+ * Returns: (transfer none): the #DhBookListDirectory:directory.
+ * Since: 3.30
+ */
+GFile *
+dh_book_list_directory_get_directory (DhBookListDirectory *list_directory)
+{
+ g_return_val_if_fail (DH_IS_BOOK_LIST_DIRECTORY (list_directory), NULL);
+
+ return list_directory->priv->directory;
+}
diff --git a/devhelp/dh-book-list-directory.h b/devhelp/dh-book-list-directory.h
new file mode 100644
index 0000000..1aade54
--- /dev/null
+++ b/devhelp/dh-book-list-directory.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * This file is part of Devhelp.
+ *
+ * Copyright (C) 2018 Sébastien Wilmet <swilmet gnome org>
+ *
+ * Devhelp is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation, either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * Devhelp 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Devhelp. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DH_BOOK_LIST_DIRECTORY_H
+#define DH_BOOK_LIST_DIRECTORY_H
+
+#include <gio/gio.h>
+#include <devhelp/dh-book-list.h>
+
+G_BEGIN_DECLS
+
+#define DH_TYPE_BOOK_LIST_DIRECTORY (dh_book_list_directory_get_type ())
+#define DH_BOOK_LIST_DIRECTORY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj),
DH_TYPE_BOOK_LIST_DIRECTORY, DhBookListDirectory))
+#define DH_BOOK_LIST_DIRECTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass),
DH_TYPE_BOOK_LIST_DIRECTORY, DhBookListDirectoryClass))
+#define DH_IS_BOOK_LIST_DIRECTORY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj),
DH_TYPE_BOOK_LIST_DIRECTORY))
+#define DH_IS_BOOK_LIST_DIRECTORY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),
DH_TYPE_BOOK_LIST_DIRECTORY))
+#define DH_BOOK_LIST_DIRECTORY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj),
DH_TYPE_BOOK_LIST_DIRECTORY, DhBookListDirectoryClass))
+
+typedef struct _DhBookListDirectory DhBookListDirectory;
+typedef struct _DhBookListDirectoryClass DhBookListDirectoryClass;
+typedef struct _DhBookListDirectoryPrivate DhBookListDirectoryPrivate;
+
+struct _DhBookListDirectory {
+ DhBookList parent;
+
+ DhBookListDirectoryPrivate *priv;
+};
+
+struct _DhBookListDirectoryClass {
+ DhBookListClass parent_class;
+
+ /* Padding for future expansion */
+ gpointer padding[12];
+};
+
+GType dh_book_list_directory_get_type (void);
+
+DhBookListDirectory * dh_book_list_directory_new (GFile *directory);
+
+GFile * dh_book_list_directory_get_directory (DhBookListDirectory *list_directory);
+
+G_END_DECLS
+
+#endif /* DH_BOOK_LIST_DIRECTORY_H */
diff --git a/devhelp/meson.build b/devhelp/meson.build
index d3093e2..57300f5 100644
--- a/devhelp/meson.build
+++ b/devhelp/meson.build
@@ -4,6 +4,7 @@ libdevhelp_public_headers = [
'dh-book.h',
'dh-book-list.h',
'dh-book-list-builder.h',
+ 'dh-book-list-directory.h',
'dh-book-manager.h',
'dh-book-tree.h',
'dh-completion.h',
@@ -22,6 +23,7 @@ libdevhelp_public_c_files = [
'dh-book.c',
'dh-book-list.c',
'dh-book-list-builder.c',
+ 'dh-book-list-directory.c',
'dh-book-manager.c',
'dh-book-tree.c',
'dh-completion.c',
diff --git a/docs/reference/devhelp-docs.xml b/docs/reference/devhelp-docs.xml
index 3e3f4ec..9486f15 100644
--- a/docs/reference/devhelp-docs.xml
+++ b/docs/reference/devhelp-docs.xml
@@ -30,6 +30,7 @@
<title>The Data</title>
<xi:include href="xml/dh-book-manager.xml"/>
<xi:include href="xml/dh-book-list.xml"/>
+ <xi:include href="xml/dh-book-list-directory.xml"/>
<xi:include href="xml/dh-book-list-builder.xml"/>
<xi:include href="xml/dh-book.xml"/>
<xi:include href="xml/dh-link.xml"/>
diff --git a/docs/reference/devhelp-sections.txt b/docs/reference/devhelp-sections.txt
index 0d9905c..7f1d466 100644
--- a/docs/reference/devhelp-sections.txt
+++ b/docs/reference/devhelp-sections.txt
@@ -89,6 +89,23 @@ dh_book_list_builder_get_type
</SECTION>
<SECTION>
+<FILE>dh-book-list-directory</FILE>
+DhBookListDirectory
+dh_book_list_directory_new
+dh_book_list_directory_get_directory
+<SUBSECTION Standard>
+DH_BOOK_LIST_DIRECTORY
+DH_BOOK_LIST_DIRECTORY_CLASS
+DH_BOOK_LIST_DIRECTORY_GET_CLASS
+DH_IS_BOOK_LIST_DIRECTORY
+DH_IS_BOOK_LIST_DIRECTORY_CLASS
+DH_TYPE_BOOK_LIST_DIRECTORY
+DhBookListDirectoryClass
+DhBookListDirectoryPrivate
+dh_book_list_directory_get_type
+</SECTION>
+
+<SECTION>
<FILE>dh-book-manager</FILE>
DhBookManager
dh_book_manager_new
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 5649132..ad19b86 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -8,6 +8,7 @@ devhelp/dh-assistant-view.c
devhelp/dh-book.c
devhelp/dh-book-list-builder.c
devhelp/dh-book-list.c
+devhelp/dh-book-list-directory.c
devhelp/dh-book-list-simple.c
devhelp/dh-book-manager.c
devhelp/dh-book-tree.c
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]