/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2002 CodeFactory AB
* Copyright (C) 2002 Mikael Hallendal <micke@imendio.com>
* Copyright (C) 2004-2008 Imendio AB
* Copyright (C) 2010 Lanedo GmbH
* Copyright (C) 2012 Thomas Bechtold <toabctl@gnome.org>
* Copyright (C) 2017 Sébastien Wilmet <swilmet@gnome.org>
*
* This program 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 2 of the
* License, or (at your option) any later version.
*
* This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "dh-book-manager.h"
#include "dh-book.h"
#include "dh-settings.h"
#include "dh-util.h"
/**
* SECTION:dh-book-manager
* @Title: DhBookManager
* @Short_description: Aggregation of all #DhBook's
*
* #DhBookManager is a singleton class containing all the #DhBook's.
*/
/* TODO: Re-architect DhBookManager and DhBook.
*
* DhBookManager and DhBook are not very flexible:
* 1. Whether a DhBook is enabled or disabled is hard-coded into the DhBook
* objects. It's bound to the "books-disabled" GSetting.
*
* 2. The list of directories where DhBookManager searches the books is more or
* less hard-coded inside DhBookManager (it's just configurable with XDG env
* variables, see the populate() function, it's documented in the README). It
* would be nice to have total control over which directories are searched,
* without duplicating them if two different "views" have a directory in common
* (especially not duplicating GFileMonitor's).
*
* Ideas:
* - Create a DhBookSelection class (or set of classes, with maybe an interface
* or base class), and remove the "enabled" property from DhBook. The
* books-disabled GSetting would be implemented by one implementation of
* DhBookSelection. A :book-selection property could be added to some classes,
* and if that property is NULL take the "default selection" (by default the
* one for the books-disabled GSetting). Another possible name: DhBookFilter
* (or have both).
*
* Have ::book-added and ::book-removed signals. A single ::changed signal is
* I think not appropriate: for example for DhBookTree, a full repopulate
* could be done when ::changed is emitted, but in that case DhBookTree would
* loose its selection.
*
* - Factor out a DhBookListDirectory class, finding and monitoring a list of
* books in one directory. The constructor would roughly be
* find_books_in_dir(). DhBookManager would just contain a list of
* DhBookListDirectory objects, ensuring that there are no duplicates. A list
* of DhBookListDirectory's could be added to a DhBookSelection, and it's
* DhBookSelection which applies priorities. So two different DhBookSelection
* objects could apply different priorities between the directories.
* Ensuring that a book ID is unique would be done by each DhBookListDirectory
* object, and also by DhBookSelection; which means that Devhelp would use
* more memory since some DhBooks would not be freed since they are contained
* in different DhBookListDirectory objects, but the index files anyway needed
* to be parsed to know the book ID, so it's not a big issue.
*
* Relevant bugzilla tickets:
* - https://bugzilla.gnome.org/show_bug.cgi?id=784491
* "BookManager: allow custom search paths for documentation"
*
* For gnome-builder needs.
*
* - https://bugzilla.gnome.org/show_bug.cgi?id=792068
* "Make it work with Flatpak"
*
* The directories probably need to be adjusted.
*
* - https://bugzilla.gnome.org/show_bug.cgi?id=761284
* "Have the latest stable/unstable GNOME API references"
*
* The books can be downloaded in different directories, one directory for
* "GNOME stable" and another directory for "GNOME unstable" (or for specific
* versions). Switching between versions would just be a matter of changing
* the DhBookSelection.
*
* - https://bugzilla.gnome.org/show_bug.cgi?id=118423
* "Individual bookshelfs"
*
* - https://bugzilla.gnome.org/show_bug.cgi?id=764441
* "Implement language switching feature"
*
* Basically have the same book ID/name available for different programming
* languages. DhBookSelection could filter by programming language. Out of
* scope for now, because language switching is implemented in JavaScript for
* hot-doc, and gtk-doc doesn't support yet producing documentation for
* different programming languages.
*/
#define NEW_POSSIBLE_BOOK_TIMEOUT_SECS 5
typedef struct {
DhBookManager *book_manager; /* unowned */
GFile *book_directory;
guint timeout_id;
} NewPossibleBookData;
typedef struct {
/* The list of all DhBooks* found in the system */
GList *books;
/* GFile* -> GFileMonitor* */
GHashTable *monitors;
/* List of NewPossibleBookData* */
GSList *new_possible_books_data;
/* List of book IDs (gchar*) currently disabled */
GList *books_disabled;
guint group_by_language : 1;
} DhBookManagerPrivate;
enum {
SIGNAL_BOOK_CREATED,
SIGNAL_BOOK_DELETED,
SIGNAL_BOOK_ENABLED,
SIGNAL_BOOK_DISABLED,
N_SIGNALS
};
enum {
PROP_0,
PROP_GROUP_BY_LANGUAGE
};
static guint signals[N_SIGNALS] = { 0 };
static DhBookManager *singleton = NULL;
G_DEFINE_TYPE_WITH_PRIVATE (DhBookManager, dh_book_manager, G_TYPE_OBJECT);
static gboolean create_book_from_index_file (DhBookManager *book_manager,
GFile *index_file);
static NewPossibleBookData *
new_possible_book_data_new (DhBookManager *book_manager,
GFile *book_directory)
{
NewPossibleBookData *data;
data = g_new0 (NewPossibleBookData, 1);
data->book_manager = book_manager;
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
dh_book_manager_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
DhBookManager *book_manager = DH_BOOK_MANAGER (object);
switch (prop_id)
{
case PROP_GROUP_BY_LANGUAGE:
g_value_set_boolean (value, dh_book_manager_get_group_by_language (book_manager));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
dh_book_manager_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
DhBookManager *book_manager = DH_BOOK_MANAGER (object);
switch (prop_id)
{
case PROP_GROUP_BY_LANGUAGE:
dh_book_manager_set_group_by_language (book_manager, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
dh_book_manager_dispose (GObject *object)
{
DhBookManagerPrivate *priv;
priv = dh_book_manager_get_instance_private (DH_BOOK_MANAGER (object));
g_list_free_full (priv->books, g_object_unref);
priv->books = NULL;
if (priv->monitors != NULL) {
g_hash_table_destroy (priv->monitors);
priv->monitors = NULL;
}
g_slist_free_full (priv->new_possible_books_data, new_possible_book_data_free);
priv->new_possible_books_data = NULL;
G_OBJECT_CLASS (dh_book_manager_parent_class)->dispose (object);
}
static void
dh_book_manager_finalize (GObject *object)
{
DhBookManager *book_manager = DH_BOOK_MANAGER (object);
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
g_list_free_full (priv->books_disabled, g_free);
if (singleton == book_manager)
singleton = NULL;
G_OBJECT_CLASS (dh_book_manager_parent_class)->finalize (object);
}
static void
dh_book_manager_class_init (DhBookManagerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->get_property = dh_book_manager_get_property;
object_class->set_property = dh_book_manager_set_property;
object_class->dispose = dh_book_manager_dispose;
object_class->finalize = dh_book_manager_finalize;
/**
* DhBookManager::book-created:
* @book_manager: the #DhBookManager.
* @book: the created #DhBook.
*/
signals[SIGNAL_BOOK_CREATED] =
g_signal_new ("book-created",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1,
DH_TYPE_BOOK);
/**
* DhBookManager::book-deleted:
* @book_manager: the #DhBookManager.
* @book: the deleted #DhBook.
*/
signals[SIGNAL_BOOK_DELETED] =
g_signal_new ("book-deleted",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1,
DH_TYPE_BOOK);
/**
* DhBookManager::book-enabled:
* @book_manager: the #DhBookManager.
* @book: the enabled #DhBook.
*/
signals[SIGNAL_BOOK_ENABLED] =
g_signal_new ("book-enabled",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1,
DH_TYPE_BOOK);
/**
* DhBookManager::book-disabled:
* @book_manager: the #DhBookManager.
* @book: the disabled #DhBook.
*/
signals[SIGNAL_BOOK_DISABLED] =
g_signal_new ("book-disabled",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1,
DH_TYPE_BOOK);
/**
* DhBookManager:group-by-language:
*
* Whether books should be grouped by programming language.
*/
g_object_class_install_property (object_class,
PROP_GROUP_BY_LANGUAGE,
g_param_spec_boolean ("group-by-language",
"Group by language",
"",
FALSE,
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
}
static void
load_books_disabled (DhBookManager *book_manager)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
DhSettings *settings;
GSettings *contents_settings;
gchar **books_disabled_strv;
gint i;
g_assert (priv->books_disabled == NULL);
settings = dh_settings_get_singleton ();
contents_settings = dh_settings_peek_contents_settings (settings);
books_disabled_strv = g_settings_get_strv (contents_settings, "books-disabled");
if (books_disabled_strv == NULL)
return;
for (i = 0; books_disabled_strv[i] != NULL; i++) {
gchar *book_id = books_disabled_strv[i];
priv->books_disabled = g_list_prepend (priv->books_disabled, book_id);
}
priv->books_disabled = g_list_reverse (priv->books_disabled);
g_free (books_disabled_strv);
}
static void
store_books_disabled (DhBookManager *book_manager)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
DhSettings *settings;
GSettings *contents_settings;
GVariantBuilder *builder;
GVariant *variant;
GList *l;
builder = g_variant_builder_new (G_VARIANT_TYPE_STRING_ARRAY);
for (l = priv->books_disabled; l != NULL; l = l->next) {
const gchar *book_id = l->data;
g_variant_builder_add (builder, "s", book_id);
}
variant = g_variant_builder_end (builder);
g_variant_builder_unref (builder);
settings = dh_settings_get_singleton ();
contents_settings = dh_settings_peek_contents_settings (settings);
g_settings_set_value (contents_settings, "books-disabled", variant);
}
static GList *
find_book_in_disabled_list (GList *books_disabled,
DhBook *book)
{
const gchar *book_id;
GList *node;
book_id = dh_book_get_id (book);
for (node = books_disabled; node != NULL; node = node->next) {
const gchar *cur_book_id = node->data;
if (g_strcmp0 (book_id, cur_book_id) == 0)
return node;
}
return NULL;
}
static gboolean
is_book_disabled_in_conf (DhBookManager *book_manager,
DhBook *book)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
return find_book_in_disabled_list (priv->books_disabled, book) != NULL;
}
static void
remove_book (DhBookManager *book_manager,
DhBook *book)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
GList *node;
node = g_list_find (priv->books, book);
if (node != NULL) {
g_signal_emit (book_manager,
signals[SIGNAL_BOOK_DELETED],
0,
book);
priv->books = g_list_delete_link (priv->books, node);
g_object_unref (book);
}
}
static void
book_deleted_cb (DhBook *book,
DhBookManager *book_manager)
{
remove_book (book_manager, book);
}
static void
book_updated_cb (DhBook *book,
DhBookManager *book_manager)
{
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);
remove_book (book_manager, book);
create_book_from_index_file (book_manager, index_file);
g_object_unref (index_file);
}
static void
book_enabled_cb (DhBook *book,
DhBookManager *book_manager)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
GList *node;
gchar *book_id;
node = find_book_in_disabled_list (priv->books_disabled, book);
/* When setting as enabled a given book, we should have it in the
* disabled books list!
*/
g_return_if_fail (node != NULL);
book_id = node->data;
g_free (book_id);
priv->books_disabled = g_list_delete_link (priv->books_disabled, node);
store_books_disabled (book_manager);
g_signal_emit (book_manager,
signals[SIGNAL_BOOK_ENABLED],
0,
book);
}
static void
book_disabled_cb (DhBook *book,
DhBookManager *book_manager)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
GList *node;
const gchar *book_id;
node = find_book_in_disabled_list (priv->books_disabled, book);
/* When setting as disabled a given book, we shouldn't have it in the
* disabled books list!
*/
g_return_if_fail (node == NULL);
book_id = dh_book_get_id (book);
priv->books_disabled = g_list_append (priv->books_disabled,
g_strdup (book_id));
store_books_disabled (book_manager);
g_signal_emit (book_manager,
signals[SIGNAL_BOOK_DISABLED],
0,
book);
}
/* 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 (DhBookManager *book_manager,
GFile *index_file)
{
DhBookManagerPrivate *priv;
DhBook *book;
gboolean book_enabled;
GList *l;
priv = dh_book_manager_get_instance_private (book_manager);
/* Check if a DhBook at the same location has already been loaded. */
for (l = priv->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 in the manager (we need
* to force unique book IDs).
*/
if (g_list_find_custom (priv->books,
book,
(GCompareFunc)dh_book_cmp_by_id)) {
g_object_unref (book);
return TRUE;
}
priv->books = g_list_insert_sorted (priv->books,
book,
(GCompareFunc)dh_book_cmp_by_title);
book_enabled = !is_book_disabled_in_conf (book_manager, book);
dh_book_set_enabled (book, book_enabled);
g_signal_connect_object (book,
"deleted",
G_CALLBACK (book_deleted_cb),
book_manager,
0);
g_signal_connect_object (book,
"updated",
G_CALLBACK (book_updated_cb),
book_manager,
0);
g_signal_connect_object (book,
"enabled",
G_CALLBACK (book_enabled_cb),
book_manager,
0);
g_signal_connect_object (book,
"disabled",
G_CALLBACK (book_disabled_cb),
book_manager,
0);
g_signal_emit (book_manager,
signals[SIGNAL_BOOK_CREATED],
0,
book);
return TRUE;
}
static void
create_book_from_directory (DhBookManager *book_manager,
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 (book_manager, 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;
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (data->book_manager);
data->timeout_id = 0;
create_book_from_directory (data->book_manager, 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,
DhBookManager *book_manager)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
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 (book_manager, 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 (DhBookManager *book_manager,
GFile *books_directory)
{
DhBookManagerPrivate *priv = dh_book_manager_get_instance_private (book_manager);
GFileMonitor *directory_monitor;
GError *error = NULL;
/* If monitor already exists, do not re-create it. */
if (priv->monitors != NULL &&
g_hash_table_lookup (priv->monitors, books_directory) != NULL) {
return;
}
directory_monitor = g_file_monitor_directory (books_directory,
G_FILE_MONITOR_NONE,
NULL,
&error);
if (error != NULL) {
gchar *parse_name;
parse_name = g_file_get_parse_name (books_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 (directory_monitor != NULL) {
if (G_UNLIKELY (priv->monitors == NULL)) {
priv->monitors = g_hash_table_new_full (g_file_hash,
(GEqualFunc) g_file_equal,
g_object_unref,
g_object_unref);
}
g_hash_table_insert (priv->monitors,
g_object_ref (books_directory),
directory_monitor);
g_signal_connect_object (directory_monitor,
"changed",
G_CALLBACK (books_directory_changed_cb),
book_manager,
0);
}
}
static void
find_books_in_dir (DhBookManager *book_manager,
const gchar *dir_path)
{
GFile *directory;
GFileEnumerator *enumerator;
GError *error = NULL;
g_return_if_fail (dir_path != NULL);
directory = g_file_new_for_path (dir_path);
enumerator = g_file_enumerate_children (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) {
g_warning ("Error when reading directory '%s': %s",
dir_path,
error->message);
g_clear_error (&error);
goto out;
}
monitor_books_directory (book_manager, directory);
while (TRUE) {
GFile *book_directory = NULL;
g_file_enumerator_iterate (enumerator, NULL, &book_directory, NULL, &error);
if (error != NULL) {
g_warning ("Error when enumerating directory '%s': %s",
dir_path,
error->message);
g_clear_error (&error);
break;
}
if (book_directory == NULL)
break;
create_book_from_directory (book_manager, book_directory);
}
out:
g_object_unref (directory);
g_clear_object (&enumerator);
}
static void
find_books_in_data_dir (DhBookManager *book_manager,
const gchar *data_dir)
{
gchar *dir;
g_return_if_fail (data_dir != NULL);
dir = g_build_filename (data_dir, "gtk-doc", "html", NULL);
find_books_in_dir (book_manager, dir);
g_free (dir);
dir = g_build_filename (data_dir, "devhelp", "books", NULL);
find_books_in_dir (book_manager, dir);
g_free (dir);
}
static void
populate (DhBookManager *book_manager)
{
const gchar * const *system_dirs;
gint i;
find_books_in_data_dir (book_manager, g_get_user_data_dir ());
system_dirs = g_get_system_data_dirs ();
g_return_if_fail (system_dirs != NULL);
for (i = 0; system_dirs[i] != NULL; i++) {
find_books_in_data_dir (book_manager, system_dirs[i]);
}
/* For Flatpak, to see the books installed on the host by traditional
* Linux distro packages.
*
* It is not a good idea to add the directory to XDG_DATA_DIRS, see:
* https://github.com/flatpak/flatpak/issues/1299
* "all sorts of things will break if we add all host config to each
* app, which is totally opposite to the entire point of flatpak."
* "i don't think XDG_DATA_DIRS is the right thing, because all sorts of
* libraries will start reading files from there, like dconf, dbus,
* service files, mimetypes, etc. It would be preferable to have
* something that targeted just gtk-doc files."
*
* So instead of adapting XDG_DATA_DIRS, add the directory here, with
* the path hard-coded.
*
* https://bugzilla.gnome.org/show_bug.cgi?id=792068
*/
#ifdef FLATPAK_BUILD
find_books_in_data_dir (book_manager, "/run/host/usr/share");
#endif
}
static void
dh_book_manager_init (DhBookManager *book_manager)
{
DhSettings *settings;
GSettings *contents_settings;
load_books_disabled (book_manager);
settings = dh_settings_get_singleton ();
contents_settings = dh_settings_peek_contents_settings (settings);
g_settings_bind (contents_settings, "group-books-by-language",
book_manager, "group-by-language",
G_SETTINGS_BIND_DEFAULT);
populate (book_manager);
}
/**
* dh_book_manager_new:
*
* Returns: (transfer full): the #DhBookManager singleton instance. You need to
* unref it when no longer needed.
* Deprecated: 3.26: Call dh_book_manager_get_singleton() instead.
*/
DhBookManager *
dh_book_manager_new (void)
{
return g_object_ref (dh_book_manager_get_singleton ());
}
/**
* dh_book_manager_get_singleton:
*
* Returns: (transfer none): the #DhBookManager singleton instance.
* Since: 3.26
*/
DhBookManager *
dh_book_manager_get_singleton (void)
{
if (singleton == NULL)
singleton = g_object_new (DH_TYPE_BOOK_MANAGER, NULL);
return singleton;
}
void
_dh_book_manager_unref_singleton (void)
{
if (singleton != NULL)
g_object_unref (singleton);
/* singleton is not set to NULL here, it is set to NULL in
* dh_book_manager_finalize() (i.e. when we are sure that the ref count
* reaches 0).
*/
}
/**
* dh_book_manager_populate:
* @book_manager: a #DhBookManager.
*
* Populates the #DhBookManager with all books found on the system and user
* directories.
*
* Deprecated: 3.26: The #DhBookManager is now automatically populated when the
* object is created, there is no need to call this function anymore.
*/
void
dh_book_manager_populate (DhBookManager *book_manager)
{
}
/**
* dh_book_manager_get_books:
* @book_manager: a #DhBookManager.
*
* Returns: (element-type DhBook) (transfer none): the list of all #DhBook's
* found.
*/
GList *
dh_book_manager_get_books (DhBookManager *book_manager)
{
DhBookManagerPrivate *priv;
g_return_val_if_fail (DH_IS_BOOK_MANAGER (book_manager), NULL);
priv = dh_book_manager_get_instance_private (book_manager);
return priv->books;
}
/**
* dh_book_manager_get_group_by_language:
* @book_manager: a #DhBookManager.
*
* Returns: whether the books should be grouped by programming language.
*/
gboolean
dh_book_manager_get_group_by_language (DhBookManager *book_manager)
{
DhBookManagerPrivate *priv;
g_return_val_if_fail (DH_IS_BOOK_MANAGER (book_manager), FALSE);
priv = dh_book_manager_get_instance_private (book_manager);
return priv->group_by_language;
}
/**
* dh_book_manager_set_group_by_language:
* @book_manager: a #DhBookManager.
* @group_by_language: the new value.
*
* Sets whether the books should be grouped by programming language.
*/
void
dh_book_manager_set_group_by_language (DhBookManager *book_manager,
gboolean group_by_language)
{
DhBookManagerPrivate *priv;
g_return_if_fail (DH_IS_BOOK_MANAGER (book_manager));
priv = dh_book_manager_get_instance_private (book_manager);
group_by_language = group_by_language != FALSE;
if (priv->group_by_language != group_by_language) {
priv->group_by_language = group_by_language;
g_object_notify (G_OBJECT (book_manager), "group-by-language");
}
}