Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
 * Copyright (C) 2001-2003 Mikael Hallendal <micke@imendio.com>
 * Copyright (C) 2003 CodeFactory AB
 * Copyright (C) 2008 Imendio AB
 * Copyright (C) 2010 Lanedo GmbH
 * Copyright (C) 2015, 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-tree.h"
#include <glib/gi18n-lib.h>
#include "dh-book-manager.h"
#include "dh-book.h"

/**
 * SECTION:dh-book-tree
 * @Title: DhBookTree
 * @Short_description: A #GtkTreeView containing the tree structure of all
 * enabled #DhBook's
 *
 * #DhBookTree is a #GtkTreeView (showing a tree, not a list) containing the
 * general tree structure of all enabled #DhBook's.
 *
 * The dh_book_get_tree() function is called to get the tree structure of a
 * #DhBook. As such the tree contains only #DhLink's of type %DH_LINK_TYPE_BOOK
 * or %DH_LINK_TYPE_PAGE.
 *
 * When an element is selected, the #DhBookTree::link-selected signal is
 * emitted. Only one element can be selected at a time.
 */

typedef struct {
        const gchar *uri;
        GtkTreeIter  iter;
        GtkTreePath *path;
        guint        found : 1;
} FindURIData;

typedef struct {
        GtkTreeStore *store;
        DhLink *selected_link;
        GtkMenu *context_menu;
} DhBookTreePrivate;

enum {
        LINK_SELECTED,
        N_SIGNALS
};

enum {
        COL_TITLE,
        COL_LINK,
        COL_BOOK,
        COL_WEIGHT,
        COL_UNDERLINE,
        N_COLUMNS
};

G_DEFINE_TYPE_WITH_PRIVATE (DhBookTree, dh_book_tree, GTK_TYPE_TREE_VIEW);

static guint signals[N_SIGNALS] = { 0 };

static void
book_tree_selection_changed_cb (GtkTreeSelection *selection,
                                DhBookTree       *tree)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        GtkTreeIter     iter;

        if (gtk_tree_selection_get_selected (selection, NULL, &iter)) {
                DhLink *link;

                gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
                                    &iter,
                                    COL_LINK, &link,
                                    -1);

                if (link != NULL &&
                    link != priv->selected_link) {
                        g_clear_pointer (&priv->selected_link, (GDestroyNotify)dh_link_unref);
                        priv->selected_link = dh_link_ref (link);
                        g_signal_emit (tree, signals[LINK_SELECTED], 0, link);
                }

                if (link != NULL)
                        dh_link_unref (link);
        }
}

static void
book_tree_setup_selection (DhBookTree *tree)
{
        GtkTreeSelection *selection;

        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));

        gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);

        g_signal_connect_object (selection,
                                 "changed",
                                 G_CALLBACK (book_tree_selection_changed_cb),
                                 tree,
                                 0);
}

/* Tries to find:
 *  - An exact match of the language group
 *  - Or the language group which should be just after our given language group.
 *  - Or both.
 *
 * FIXME: not great code. Maybe add a new column in the GtkTreeModel storing a
 * DhLanguage object. Instead of @language as a string, it would be a
 * DhLanguage.
 */
static void
book_tree_find_language_group (DhBookTree  *tree,
                               const gchar *language,
                               GtkTreeIter *exact_iter,
                               gboolean    *exact_found,
                               GtkTreeIter *next_iter,
                               gboolean    *next_found)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        DhBookManager *book_manager;
        GtkTreeIter loop_iter;

        g_assert ((exact_iter != NULL && exact_found != NULL) ||
                  (next_iter != NULL && next_found != NULL));

        /* Reset all flags to not found */
        if (exact_found != NULL)
                *exact_found = FALSE;
        if (next_found != NULL)
                *next_found = FALSE;

        /* If we're not doing language grouping, return not found */
        book_manager = dh_book_manager_get_singleton ();
        if (!dh_book_manager_get_group_by_language (book_manager))
                return;

        if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
                                            &loop_iter)) {
                /* Store is empty, not found */
                return;
        }

        do {
                gchar *title = NULL;
                DhLink *link;

                /* Look for language titles, which are those where there
                 * is no book object associated in the row */
                gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
                                    &loop_iter,
                                    COL_TITLE, &title,
                                    COL_LINK, &link,
                                    -1);

                if (link != NULL) {
                        /* Not a language */
                        g_free (title);
                        dh_link_unref (link);
                        g_return_if_reached ();
                }

                if (exact_iter != NULL && exact_found &&
                    g_ascii_strcasecmp (title, language) == 0) {
                        /* Exact match found! */
                        *exact_iter = loop_iter;
                        *exact_found = TRUE;
                        if (next_iter == NULL) {
                                /* If we were not requested to look for the next one, end here */
                                g_free (title);
                                return;
                        }
                } else if (next_iter != NULL && next_found &&
                           g_ascii_strcasecmp (title, language) > 0) {
                        *next_iter = loop_iter;
                        *next_found = TRUE;
                        /* There's no way to have an exact match after the next, so end here */
                        g_free (title);
                        return;
                }

                g_free (title);
        } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->store),
                                           &loop_iter));
}

/* Tries to find, starting at 'first' (if given), and always in the same
 * level of the tree:
 *  - An exact match of the book
 *  - Or the book which should be just after our given book
 *  - Or both.
 */
static void
book_tree_find_book (DhBookTree        *tree,
                     DhBook            *book,
                     const GtkTreeIter *first,
                     GtkTreeIter       *exact_iter,
                     gboolean          *exact_found,
                     GtkTreeIter       *next_iter,
                     gboolean          *next_found)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        GtkTreeIter     loop_iter;

        g_assert ((exact_iter != NULL && exact_found != NULL) ||
                  (next_iter != NULL && next_found != NULL));

        /* Reset all flags to not found */
        if (exact_found != NULL)
                *exact_found = FALSE;
        if (next_found != NULL)
                *next_found = FALSE;

        /* Setup iteration start */
        if (first == NULL) {
                /* If no first given, start iterating from the start of the model */
                if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
                                                    &loop_iter)) {
                        /* Store is empty, not found */
                        return;
                }
        } else {
                loop_iter = *first;
        }

        do {
                DhBook *in_tree_book = NULL;

                gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
                                    &loop_iter,
                                    COL_BOOK, &in_tree_book,
                                    -1);

                g_return_if_fail (DH_IS_BOOK (in_tree_book));

                /* We can compare pointers directly as we're playing with references
                 * of the same object */
                if (exact_iter != NULL && exact_found &&
                    in_tree_book == book) {
                        *exact_iter = loop_iter;
                        *exact_found = TRUE;
                        if (next_iter == NULL) {
                                /* If we were not requested to look for the next one, end here */
                                g_object_unref (in_tree_book);
                                return;
                        }
                } else if (next_iter != NULL &&
                           dh_book_cmp_by_title (in_tree_book, book) > 0) {
                        *next_iter = loop_iter;
                        *next_found = TRUE;
                        g_object_unref (in_tree_book);
                        return;
                }

                g_object_unref (in_tree_book);
        } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->store),
                                           &loop_iter));
}

static void
book_tree_insert_node (DhBookTree  *tree,
                       GNode       *node,
                       GtkTreeIter *current_iter,
                       DhBook      *book)

{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        DhLink         *link;
        PangoWeight     weight;
        GNode          *child;

        link = node->data;
        g_assert (link != NULL);

        if (dh_link_get_link_type (link) == DH_LINK_TYPE_BOOK) {
                weight = PANGO_WEIGHT_BOLD;
        } else {
                weight = PANGO_WEIGHT_NORMAL;
        }

        gtk_tree_store_set (priv->store,
                            current_iter,
                            COL_TITLE, dh_link_get_name (link),
                            COL_LINK, link,
                            COL_BOOK, book,
                            COL_WEIGHT, weight,
                            COL_UNDERLINE, PANGO_UNDERLINE_NONE,
                            -1);

        for (child = g_node_first_child (node);
             child != NULL;
             child = g_node_next_sibling (child)) {
                GtkTreeIter iter;

                /* Append new iter */
                gtk_tree_store_append (priv->store, &iter, current_iter);
                book_tree_insert_node (tree, child, &iter, NULL);
        }
}

static void
book_tree_add_book_to_store (DhBookTree *tree,
                             DhBook     *book)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        DhBookManager *book_manager;
        GtkTreeIter book_iter;

        /* If grouping by language we need to add the language categories */
        book_manager = dh_book_manager_get_singleton ();
        if (dh_book_manager_get_group_by_language (book_manager)) {
                GtkTreeIter  language_iter;
                gboolean     language_iter_found;
                GtkTreeIter  next_language_iter;
                gboolean     next_language_iter_found;
                const gchar *language_title;
                gboolean     new_language = FALSE;

                language_title = dh_book_get_language (book);

                /* Look for the proper language group */
                book_tree_find_language_group (tree,
                                               language_title,
                                               &language_iter,
                                               &language_iter_found,
                                               &next_language_iter,
                                               &next_language_iter_found);
                /* New language group needs to be created? */
                if (!language_iter_found) {
                        if (!next_language_iter_found) {
                                gtk_tree_store_append (priv->store,
                                                       &language_iter,
                                                       NULL);
                        } else {
                                gtk_tree_store_insert_before (priv->store,
                                                              &language_iter,
                                                              NULL,
                                                              &next_language_iter);
                        }

                        gtk_tree_store_set (priv->store,
                                            &language_iter,
                                            COL_TITLE,      language_title,
                                            COL_LINK,       NULL,
                                            COL_BOOK,       NULL,
                                            COL_WEIGHT,     PANGO_WEIGHT_BOLD,
                                            COL_UNDERLINE,  PANGO_UNDERLINE_SINGLE,
                                            -1);

                        new_language = TRUE;
                }

                /* If we got to add first book in a given language group, just append it. */
                if (new_language) {
                        GtkTreePath *path;

                        gtk_tree_store_append (priv->store,
                                               &book_iter,
                                               &language_iter);

                        /* Make sure we start with the language row expanded */
                        path = gtk_tree_model_get_path (GTK_TREE_MODEL (priv->store),
                                                        &language_iter);
                        gtk_tree_view_expand_row (GTK_TREE_VIEW (tree),
                                                  path,
                                                  FALSE);
                        gtk_tree_path_free (path);
                } else {
                        GtkTreeIter first_book_iter;
                        GtkTreeIter next_book_iter;
                        gboolean    next_book_iter_found;

                        /* The language will have at least one book, so we move iter to it */
                        gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
                                                      &first_book_iter,
                                                      &language_iter);

                        /* Find next possible book in language group */
                        book_tree_find_book (tree,
                                             book,
                                             &first_book_iter,
                                             NULL,
                                             NULL,
                                             &next_book_iter,
                                             &next_book_iter_found);

                        if (!next_book_iter_found) {
                                gtk_tree_store_append (priv->store,
                                                       &book_iter,
                                                       &language_iter);
                        } else {
                                gtk_tree_store_insert_before (priv->store,
                                                              &book_iter,
                                                              &language_iter,
                                                              &next_book_iter);
                        }
                }
        } else {
                /* No language grouping, just order by book title */
                GtkTreeIter next_book_iter;
                gboolean    next_book_iter_found;

                book_tree_find_book (tree,
                                     book,
                                     NULL,
                                     NULL,
                                     NULL,
                                     &next_book_iter,
                                     &next_book_iter_found);

                if (!next_book_iter_found) {
                        gtk_tree_store_append (priv->store,
                                               &book_iter,
                                               NULL);
                } else {
                        gtk_tree_store_insert_before (priv->store,
                                                      &book_iter,
                                                      NULL,
                                                      &next_book_iter);
                }
        }

        /* Now book_iter contains the proper iterator where we'll add the whole
         * book tree. */
        book_tree_insert_node (tree,
                               dh_book_get_tree (book),
                               &book_iter,
                               book);
}

static void
book_tree_book_created_or_enabled_cb (DhBookManager *book_manager,
                                      DhBook        *book,
                                      DhBookTree    *tree)
{
        if (!dh_book_get_enabled (book))
                return;

        book_tree_add_book_to_store (tree, book);
}

static void
book_tree_book_deleted_or_disabled_cb (DhBookManager *book_manager,
                                       DhBook        *book,
                                       DhBookTree    *tree)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        GtkTreeIter     exact_iter;
        gboolean        exact_iter_found = FALSE;
        GtkTreeIter     language_iter;
        gboolean        language_iter_found = FALSE;

        if (dh_book_manager_get_group_by_language (book_manager)) {
                GtkTreeIter first_book_iter;

                book_tree_find_language_group (tree,
                                               dh_book_get_language (book),
                                               &language_iter,
                                               &language_iter_found,
                                               NULL,
                                               NULL);

                if (language_iter_found &&
                    gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
                                                  &first_book_iter,
                                                  &language_iter)) {
                        book_tree_find_book (tree,
                                             book,
                                             &first_book_iter,
                                             &exact_iter,
                                             &exact_iter_found,
                                             NULL,
                                             NULL);
                }
        } else {
                book_tree_find_book (tree,
                                     book,
                                     NULL,
                                     &exact_iter,
                                     &exact_iter_found,
                                     NULL,
                                     NULL);
        }

        if (exact_iter_found) {
                /* Remove the book from the tree */
                gtk_tree_store_remove (priv->store, &exact_iter);
                /* If this book was inside a language group, check if the group
                 * is now empty and so removable */
                if (language_iter_found) {
                        GtkTreeIter first_book_iter;

                        if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
                                                           &first_book_iter,
                                                           &language_iter)) {
                                /* Oh, well, no more books in this language... remove! */
                                gtk_tree_store_remove (priv->store, &language_iter);
                        }
                }
        }
}

static void
book_tree_init_selection (DhBookTree *tree)
{
        DhBookTreePrivate   *priv;
        DhBookManager    *book_manager;
        GtkTreeSelection *selection;
        GtkTreeIter       iter;
        gboolean          iter_found = FALSE;

        priv = dh_book_tree_get_instance_private (tree);

        /* Mark the first item as selected, or it would get automatically
         * selected when the treeview will get focus; but that's not even
         * enough as a selection changed would still be emitted when there
         * is no change, hence the manual tracking of selection in
         * selected_link.
         *   https://bugzilla.gnome.org/show_bug.cgi?id=492206
         */
        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
        g_signal_handlers_block_by_func (selection,
                                         book_tree_selection_changed_cb,
                                         tree);

        /* If grouping by languages, get first book in the first language */
        book_manager = dh_book_manager_get_singleton ();
        if (dh_book_manager_get_group_by_language (book_manager)) {
                GtkTreeIter language_iter;

                if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
                                                   &language_iter)) {
                        iter_found = gtk_tree_model_iter_children (GTK_TREE_MODEL (priv->store),
                                                                   &iter,
                                                                   &language_iter);
                }
        } else {
                iter_found = gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->store),
                                                            &iter);
        }

        if (iter_found) {
                DhLink *link;

                gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
                                    &iter,
                                    COL_LINK, &link,
                                    -1);

                g_clear_pointer (&priv->selected_link, (GDestroyNotify)dh_link_unref);
                priv->selected_link = link;
                gtk_tree_selection_select_iter (selection, &iter);

                if (dh_link_get_link_type (link) != DH_LINK_TYPE_BOOK)
                        g_warn_if_reached ();
        }

        g_signal_handlers_unblock_by_func (selection,
                                           book_tree_selection_changed_cb,
                                           tree);
}

static void
book_tree_populate_tree (DhBookTree *tree)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);
        DhBookManager *book_manager;
        GList *l;

        gtk_tree_view_set_model (GTK_TREE_VIEW (tree), NULL);
        gtk_tree_store_clear (priv->store);
        gtk_tree_view_set_model (GTK_TREE_VIEW (tree),
                                 GTK_TREE_MODEL (priv->store));

        /* This list comes in order, but we don't really mind */
        book_manager = dh_book_manager_get_singleton ();
        for (l = dh_book_manager_get_books (book_manager);
             l != NULL;
             l = l->next) {
                DhBook *book = DH_BOOK (l->data);

                /* Only add enabled books to the tree */
                if (dh_book_get_enabled (book))
                        book_tree_add_book_to_store (tree, book);
        }

        book_tree_init_selection (tree);
}

static void
book_tree_group_by_language_cb (GObject    *object,
                                GParamSpec *pspec,
                                DhBookTree *tree)
{
        book_tree_populate_tree (tree);
}

static void
dh_book_tree_dispose (GObject *object)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (DH_BOOK_TREE (object));

        g_clear_object (&priv->store);
        g_clear_pointer (&priv->selected_link, (GDestroyNotify)dh_link_unref);
        priv->context_menu = NULL;

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

static void
collapse_all_activate_cb (GtkMenuItem *menu_item,
                          DhBookTree  *tree)
{
        gtk_tree_view_collapse_all (GTK_TREE_VIEW (tree));
}

static void
do_popup_menu (DhBookTree     *tree,
               GdkEventButton *event)
{
        DhBookTreePrivate *priv = dh_book_tree_get_instance_private (tree);

        if (priv->context_menu == NULL) {
                GtkWidget *menu_item;

                /* Create the menu only once. At first I wanted to create a new
                 * menu each time this function is called, connect to the
                 * GtkMenuShell::deactivate signal to call gtk_widget_destroy().
                 * But GtkMenuShell::deactivate is emitted before
                 * collapse_all_activate_cb(), so collapse_all_activate_cb() was
                 * never called... It's maybe a GTK+ bug.
                 */
                priv->context_menu = GTK_MENU (gtk_menu_new ());

                /* When tree is destroyed, the context menu is destroyed too. */
                gtk_menu_attach_to_widget (priv->context_menu, GTK_WIDGET (tree), NULL);

                menu_item = gtk_menu_item_new_with_mnemonic (_("_Collapse All"));
                gtk_menu_shell_append (GTK_MENU_SHELL (priv->context_menu), menu_item);
                gtk_widget_show (menu_item);

                g_signal_connect_object (menu_item,
                                         "activate",
                                         G_CALLBACK (collapse_all_activate_cb),
                                         tree,
                                         0);
        }

        if (event != NULL) {
                gtk_menu_popup_at_pointer (priv->context_menu, (GdkEvent *) event);
        } else {
                gtk_menu_popup_at_widget (priv->context_menu,
                                          GTK_WIDGET (tree),
                                          GDK_GRAVITY_NORTH_EAST,
                                          GDK_GRAVITY_NORTH_WEST,
                                          NULL);
        }
}

static gboolean
dh_book_tree_button_press_event (GtkWidget      *widget,
                                 GdkEventButton *event)
{
        DhBookTree *tree = DH_BOOK_TREE (widget);

        if (gdk_event_triggers_context_menu ((GdkEvent *) event) &&
            event->type == GDK_BUTTON_PRESS) {
                do_popup_menu (tree, event);
                return GDK_EVENT_STOP;
        }

        if (GTK_WIDGET_CLASS (dh_book_tree_parent_class)->button_press_event != NULL)
                return GTK_WIDGET_CLASS (dh_book_tree_parent_class)->button_press_event (widget, event);

        return GDK_EVENT_PROPAGATE;
}

static gboolean
dh_book_tree_popup_menu (GtkWidget *widget)
{
        if (GTK_WIDGET_CLASS (dh_book_tree_parent_class)->popup_menu != NULL)
                g_warning ("%s(): chain-up?", G_STRFUNC);

        do_popup_menu (DH_BOOK_TREE (widget), NULL);
        return TRUE;
}

static void
dh_book_tree_class_init (DhBookTreeClass *klass)
{
        GObjectClass *object_class = G_OBJECT_CLASS (klass);
        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

        object_class->dispose = dh_book_tree_dispose;

        widget_class->button_press_event = dh_book_tree_button_press_event;
        widget_class->popup_menu = dh_book_tree_popup_menu;

        /**
         * DhBookTree::link-selected:
         * @tree: the #DhBookTree.
         * @link: the selected #DhLink.
         */
        signals[LINK_SELECTED] =
                g_signal_new ("link-selected",
                              G_TYPE_FROM_CLASS (klass),
                              G_SIGNAL_RUN_LAST,
                              0,
                              NULL, NULL, NULL,
                              G_TYPE_NONE,
                              1, DH_TYPE_LINK);
}

static void
book_tree_add_columns (DhBookTree *tree)
{
        GtkCellRenderer   *cell;
        GtkTreeViewColumn *column;

        column = gtk_tree_view_column_new ();

        cell = gtk_cell_renderer_text_new ();
        g_object_set (cell,
                      "ellipsize", PANGO_ELLIPSIZE_END,
                      NULL);
        gtk_tree_view_column_pack_start (column, cell, TRUE);
        gtk_tree_view_column_set_attributes (column, cell,
                                             "text", COL_TITLE,
                                             "weight", COL_WEIGHT,
                                             "underline", COL_UNDERLINE,
                                             NULL);

        gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
}

static void
dh_book_tree_init (DhBookTree *tree)
{
        DhBookTreePrivate *priv;
        DhBookManager *book_manager;

        priv = dh_book_tree_get_instance_private (tree);

        gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tree), FALSE);
        gtk_tree_view_set_enable_search (GTK_TREE_VIEW (tree), FALSE);

        priv->store = gtk_tree_store_new (N_COLUMNS,
                                          G_TYPE_STRING, /* Title */
                                          DH_TYPE_LINK,
                                          DH_TYPE_BOOK,
                                          PANGO_TYPE_WEIGHT,
                                          PANGO_TYPE_UNDERLINE);
        priv->selected_link = NULL;
        gtk_tree_view_set_model (GTK_TREE_VIEW (tree),
                                 GTK_TREE_MODEL (priv->store));

        book_tree_add_columns (tree);
        book_tree_setup_selection (tree);

        book_manager = dh_book_manager_get_singleton ();

        g_signal_connect_object (book_manager,
                                 "book-created",
                                 G_CALLBACK (book_tree_book_created_or_enabled_cb),
                                 tree,
                                 0);

        g_signal_connect_object (book_manager,
                                 "book-enabled",
                                 G_CALLBACK (book_tree_book_created_or_enabled_cb),
                                 tree,
                                 0);

        g_signal_connect_object (book_manager,
                                 "book-deleted",
                                 G_CALLBACK (book_tree_book_deleted_or_disabled_cb),
                                 tree,
                                 0);

        g_signal_connect_object (book_manager,
                                 "book-disabled",
                                 G_CALLBACK (book_tree_book_deleted_or_disabled_cb),
                                 tree,
                                 0);

        g_signal_connect_object (book_manager,
                                 "notify::group-by-language",
                                 G_CALLBACK (book_tree_group_by_language_cb),
                                 tree,
                                 0);

        book_tree_populate_tree (tree);
}

/**
 * dh_book_tree_new:
 *
 * Returns: (transfer floating): a new #DhBookTree widget.
 */
DhBookTree *
dh_book_tree_new (void)
{
        return g_object_new (DH_TYPE_BOOK_TREE, NULL);
}

static gboolean
book_tree_find_uri_foreach (GtkTreeModel *model,
                            GtkTreePath  *path,
                            GtkTreeIter  *iter,
                            FindURIData  *data)
{
        DhLink *link;

        gtk_tree_model_get (model, iter,
                            COL_LINK, &link,
                            -1);

        if (link != NULL) {
                gchar *link_uri;

                link_uri = dh_link_get_uri (link);

                if (link_uri != NULL &&
                    g_str_has_prefix (data->uri, link_uri)) {
                        data->found = TRUE;
                        data->iter = *iter;
                        data->path = gtk_tree_path_copy (path);
                }

                g_free (link_uri);
                dh_link_unref (link);
        }

        return data->found;
}

/**
 * dh_book_tree_select_uri:
 * @tree: a #DhBookTree.
 * @uri: the URI to select.
 *
 * Selects the given @uri.
 */
void
dh_book_tree_select_uri (DhBookTree  *tree,
                         const gchar *uri)
{
        DhBookTreePrivate   *priv = dh_book_tree_get_instance_private (tree);
        GtkTreeSelection *selection;
        FindURIData       data;
        DhLink           *link;

        data.found = FALSE;
        data.uri = uri;

        gtk_tree_model_foreach (GTK_TREE_MODEL (priv->store),
                                (GtkTreeModelForeachFunc) book_tree_find_uri_foreach,
                                &data);

        if (!data.found)
                return;

        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));

        /* Do not re-select (which will expand current additionally) if already
         * there. */
        if (gtk_tree_selection_iter_is_selected (selection, &data.iter))
                goto out;

        /* FIXME: it's strange to block the signal here. The signal handler
         * should probably be blocked in DhWindow instead.
         */
        g_signal_handlers_block_by_func (selection,
                                         book_tree_selection_changed_cb,
                                         tree);

        gtk_tree_view_expand_to_path (GTK_TREE_VIEW (tree), data.path);

        gtk_tree_model_get (GTK_TREE_MODEL (priv->store),
                            &data.iter,
                            COL_LINK, &link,
                            -1);
        g_clear_pointer (&priv->selected_link, (GDestroyNotify)dh_link_unref);
        priv->selected_link = link;
        gtk_tree_selection_select_iter (selection, &data.iter);

        gtk_tree_view_set_cursor (GTK_TREE_VIEW (tree), data.path, NULL, 0);

        g_signal_handlers_unblock_by_func (selection,
                                           book_tree_selection_changed_cb,
                                           tree);

out:
        gtk_tree_path_free (data.path);
}

/**
 * dh_book_tree_get_selected_book:
 * @tree: a #DhBookTree.
 *
 * Returns: (nullable) (transfer full): the #DhLink of the selected book, or
 * %NULL if there is no selection. Unref with dh_link_unref() when no longer
 * needed.
 */
DhLink *
dh_book_tree_get_selected_book (DhBookTree *tree)
{
        GtkTreeSelection *selection;
        GtkTreeModel     *model;
        GtkTreeIter       iter;

        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
        if (!gtk_tree_selection_get_selected (selection, &model, &iter))
                return NULL;

        /* Depending on whether books are grouped by language, the book link can
         * be at a different depth. And it's safer to check that the returned
         * link has the good type. So walk up the tree to find the book.
         */
        while (TRUE) {
                DhLink *link;
                GtkTreeIter parent;

                gtk_tree_model_get (model, &iter,
                                    COL_LINK, &link,
                                    -1);

                if (dh_link_get_link_type (link) == DH_LINK_TYPE_BOOK)
                        return link;

                dh_link_unref (link);

                if (!gtk_tree_model_iter_parent (model, &parent, &iter))
                        break;

                iter = parent;
        }

        g_return_val_if_reached (NULL);
}