/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ /* * Copyright (C) 2001-2003 Mikael Hallendal * Copyright (C) 2003 CodeFactory AB * Copyright (C) 2008 Imendio AB * Copyright (C) 2010 Lanedo GmbH * Copyright (C) 2015, 2017 Sébastien Wilmet * * 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 . */ #include "config.h" #include "dh-book-tree.h" #include #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); }