From 91c487b413118ce678e06f07aa9c64df1e09bec7 Mon Sep 17 00:00:00 2001 From: Packit Date: Sep 17 2020 19:50:11 +0000 Subject: Apply patch covscan.patch patch_name: covscan.patch present_in_specfile: true --- diff --git a/src/dh-book-tree.c b/src/dh-book-tree.c index e97fe28..7d8f239 100644 --- a/src/dh-book-tree.c +++ b/src/dh-book-tree.c @@ -177,7 +177,7 @@ book_tree_find_language_group (DhBookTree *tree, g_return_if_reached (); } - if (exact_iter != NULL && + if (exact_iter != NULL && exact_found && g_ascii_strcasecmp (title, language) == 0) { /* Exact match found! */ *exact_iter = loop_iter; @@ -187,7 +187,7 @@ book_tree_find_language_group (DhBookTree *tree, g_free (title); return; } - } else if (next_iter != NULL && + } else if (next_iter != NULL && next_found && g_ascii_strcasecmp (title, language) > 0) { *next_iter = loop_iter; *next_found = TRUE; @@ -252,7 +252,7 @@ book_tree_find_book (DhBookTree *tree, /* We can compare pointers directly as we're playing with references * of the same object */ - if (exact_iter != NULL && + if (exact_iter != NULL && exact_found && in_tree_book == book) { *exact_iter = loop_iter; *exact_found = TRUE; diff --git a/src/dh-book-tree.c.covscan b/src/dh-book-tree.c.covscan new file mode 100644 index 0000000..e97fe28 --- /dev/null +++ b/src/dh-book-tree.c.covscan @@ -0,0 +1,957 @@ +/* -*- 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 && + 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 && + 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 && + 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); +} diff --git a/src/dh-keyword-model.c b/src/dh-keyword-model.c index a0b716f..6a7fc50 100644 --- a/src/dh-keyword-model.c +++ b/src/dh-keyword-model.c @@ -442,6 +442,14 @@ search_single_book (DhBook *book, return ret; } +static gint +compare_links (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + return dh_link_compare (a, b); +} + static GQueue * search_books (SearchSettings *settings, guint max_hits, @@ -486,7 +494,7 @@ search_books (SearchSettings *settings, dh_util_queue_concat (ret, book_result); } - g_queue_sort (ret, (GCompareDataFunc) dh_link_compare, NULL); + g_queue_sort (ret, (GCompareDataFunc) compare_links, NULL); return ret; } diff --git a/src/dh-keyword-model.c.covscan b/src/dh-keyword-model.c.covscan new file mode 100644 index 0000000..a0b716f --- /dev/null +++ b/src/dh-keyword-model.c.covscan @@ -0,0 +1,736 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2002 CodeFactory AB + * Copyright (C) 2002 Mikael Hallendal + * Copyright (C) 2008 Imendio AB + * Copyright (C) 2010 Lanedo GmbH + * Copyright (C) 2015-2018 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 "dh-keyword-model.h" +#include +#include "dh-book.h" +#include "dh-book-manager.h" +#include "dh-search-context.h" +#include "dh-util.h" + +/** + * SECTION:dh-keyword-model + * @Title: DhKeywordModel + * @Short_description: A custom #GtkTreeModel implementation for searching + * #DhLink's + * + * #DhKeywordModel is a custom #GtkTreeModel implementation (as a list, not a + * tree) for searching #DhLink's. + * + * The dh_keyword_model_filter() function is used to set the search criteria. It + * fills the #GtkTreeModel with the list of #DhLink's that match the search + * criteria (up to a certain maximum number of matches). + * + * How the search works (for end users) is explained in the user documentation + * of the Devhelp application. + * + * # Filter by book and page + * + * As a kind of API for integrating Devhelp with other applications, the search + * string supports additional features. Those features are not intended to be + * used directly by end users when typing the search string in the GUI, because + * it's not really convenient. It is intended to be used with the + * `devhelp --search "search-string"` command line, so that another application + * can launch Devhelp and set a specific search string. + * + * It is possible to filter by book by prefixing the search string with + * “book:the-book-ID”. For example “book:gtk3”. If there are no other search + * terms, it shows the top-level page of that book. If there are other search + * terms, it limits the search to the specified book. See also the + * dh_book_get_id() function (in the `*.devhelp2` index file format it's called + * the book “name”, not ID, but ID is clearer). + * + * Similarly, it is possible to filter by page, by prefixing the search string + * with “page:the-page-ID”. For example “page:GtkWindow”. If there are no other + * search terms, the top of the page is shown and the search matches all the + * symbols part of that page. If there are other search terms, it limits the + * search to the specified page. To know what is the “page ID”, see the + * dh_link_belongs_to_page() function. + * + * “book:” and “page:” can be combined. Normal search terms must be + * after “book:” and “page:”. + * + * The book and page IDs – even if they contain an uppercase letter – don't + * affect the case sensitivity for the other search terms. + */ + +typedef struct { + gchar *current_book_id; + + /* List of owned DhLink*. + * + * Note: GQueue, not GQueue* so we are sure that it always exists, we + * don't need to check if priv->links == NULL. + */ + GQueue links; + + gint stamp; +} DhKeywordModelPrivate; + +typedef struct { + DhSearchContext *search_context; + const gchar *book_id; + const gchar *skip_book_id; + guint prefix : 1; +} SearchSettings; + +#define MAX_HITS 1000 + +static void dh_keyword_model_tree_model_init (GtkTreeModelIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DhKeywordModel, dh_keyword_model, G_TYPE_OBJECT, + G_ADD_PRIVATE (DhKeywordModel) + G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, + dh_keyword_model_tree_model_init)); + +static void +clear_links (DhKeywordModel *model) +{ + DhKeywordModelPrivate *priv = dh_keyword_model_get_instance_private (model); + GList *l; + + for (l = priv->links.head; l != NULL; l = l->next) { + DhLink *cur_link = l->data; + dh_link_unref (cur_link); + } + + g_queue_clear (&priv->links); +} + +static void +dh_keyword_model_finalize (GObject *object) +{ + DhKeywordModel *model = DH_KEYWORD_MODEL (object); + DhKeywordModelPrivate *priv = dh_keyword_model_get_instance_private (model); + + g_free (priv->current_book_id); + clear_links (model); + + G_OBJECT_CLASS (dh_keyword_model_parent_class)->finalize (object); +} + +static void +dh_keyword_model_class_init (DhKeywordModelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = dh_keyword_model_finalize; +} + +static void +dh_keyword_model_init (DhKeywordModel *model) +{ + DhKeywordModelPrivate *priv = dh_keyword_model_get_instance_private (model); + + priv->stamp = g_random_int_range (1, G_MAXINT32); +} + +static GtkTreeModelFlags +dh_keyword_model_get_flags (GtkTreeModel *tree_model) +{ + return GTK_TREE_MODEL_LIST_ONLY; +} + +static gint +dh_keyword_model_get_n_columns (GtkTreeModel *tree_model) +{ + return DH_KEYWORD_MODEL_NUM_COLS; +} + +static GType +dh_keyword_model_get_column_type (GtkTreeModel *tree_model, + gint column) +{ + switch (column) { + case DH_KEYWORD_MODEL_COL_NAME: + return G_TYPE_STRING; + + case DH_KEYWORD_MODEL_COL_LINK: + return DH_TYPE_LINK; + + case DH_KEYWORD_MODEL_COL_CURRENT_BOOK_FLAG: + return G_TYPE_BOOLEAN; + + default: + return G_TYPE_INVALID; + } +} + +static gboolean +dh_keyword_model_get_iter (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreePath *path) +{ + DhKeywordModelPrivate *priv; + const gint *indices; + GList *node; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + if (gtk_tree_path_get_depth (path) > 1) { + return FALSE; + } + + indices = gtk_tree_path_get_indices (path); + + if (indices == NULL) { + return FALSE; + } + + node = g_queue_peek_nth_link (&priv->links, indices[0]); + + if (node != NULL) { + iter->stamp = priv->stamp; + iter->user_data = node; + return TRUE; + } + + return FALSE; +} + +static GtkTreePath * +dh_keyword_model_get_path (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + DhKeywordModelPrivate *priv; + GList *node; + GtkTreePath *path; + gint pos; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + g_return_val_if_fail (iter->stamp == priv->stamp, NULL); + + node = iter->user_data; + pos = g_queue_link_index (&priv->links, node); + + if (pos < 0) { + return NULL; + } + + path = gtk_tree_path_new (); + gtk_tree_path_append_index (path, pos); + + return path; +} + +static void +dh_keyword_model_get_value (GtkTreeModel *tree_model, + GtkTreeIter *iter, + gint column, + GValue *value) +{ + DhKeywordModelPrivate *priv; + GList *node; + DhLink *link; + gboolean in_current_book; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + g_return_if_fail (iter->stamp == priv->stamp); + + node = iter->user_data; + link = node->data; + + switch (column) { + case DH_KEYWORD_MODEL_COL_NAME: + g_value_init (value, G_TYPE_STRING); + g_value_set_string (value, dh_link_get_name (link)); + break; + + case DH_KEYWORD_MODEL_COL_LINK: + g_value_init (value, DH_TYPE_LINK); + g_value_set_boxed (value, link); + break; + + case DH_KEYWORD_MODEL_COL_CURRENT_BOOK_FLAG: + in_current_book = g_strcmp0 (dh_link_get_book_id (link), priv->current_book_id) == 0; + g_value_init (value, G_TYPE_BOOLEAN); + g_value_set_boolean (value, in_current_book); + break; + + default: + g_warning ("Bad column %d requested", column); + } +} + +static gboolean +dh_keyword_model_iter_next (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + DhKeywordModelPrivate *priv; + GList *node; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + g_return_val_if_fail (priv->stamp == iter->stamp, FALSE); + + node = iter->user_data; + iter->user_data = node->next; + + return iter->user_data != NULL; +} + +static gboolean +dh_keyword_model_iter_children (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent) +{ + DhKeywordModelPrivate *priv; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + /* This is a list, nodes have no children. */ + if (parent != NULL) { + return FALSE; + } + + /* But if parent == NULL we return the list itself as children of + * the "root". + */ + if (priv->links.head != NULL) { + iter->stamp = priv->stamp; + iter->user_data = priv->links.head; + return TRUE; + } + + return FALSE; +} + +static gboolean +dh_keyword_model_iter_has_child (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + return FALSE; +} + +static gint +dh_keyword_model_iter_n_children (GtkTreeModel *tree_model, + GtkTreeIter *iter) +{ + DhKeywordModelPrivate *priv; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + if (iter == NULL) { + return priv->links.length; + } + + g_return_val_if_fail (priv->stamp == iter->stamp, -1); + + return 0; +} + +static gboolean +dh_keyword_model_iter_nth_child (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *parent, + gint n) +{ + DhKeywordModelPrivate *priv; + GList *child; + + priv = dh_keyword_model_get_instance_private (DH_KEYWORD_MODEL (tree_model)); + + if (parent != NULL) { + return FALSE; + } + + child = g_queue_peek_nth_link (&priv->links, n); + + if (child != NULL) { + iter->stamp = priv->stamp; + iter->user_data = child; + return TRUE; + } + + return FALSE; +} + +static gboolean +dh_keyword_model_iter_parent (GtkTreeModel *tree_model, + GtkTreeIter *iter, + GtkTreeIter *child) +{ + return FALSE; +} + +static void +dh_keyword_model_tree_model_init (GtkTreeModelIface *iface) +{ + iface->get_flags = dh_keyword_model_get_flags; + iface->get_n_columns = dh_keyword_model_get_n_columns; + iface->get_column_type = dh_keyword_model_get_column_type; + iface->get_iter = dh_keyword_model_get_iter; + iface->get_path = dh_keyword_model_get_path; + iface->get_value = dh_keyword_model_get_value; + iface->iter_next = dh_keyword_model_iter_next; + iface->iter_children = dh_keyword_model_iter_children; + iface->iter_has_child = dh_keyword_model_iter_has_child; + iface->iter_n_children = dh_keyword_model_iter_n_children; + iface->iter_nth_child = dh_keyword_model_iter_nth_child; + iface->iter_parent = dh_keyword_model_iter_parent; +} + +/** + * dh_keyword_model_new: + * + * Returns: a new #DhKeywordModel object. + */ +DhKeywordModel * +dh_keyword_model_new (void) +{ + return g_object_new (DH_TYPE_KEYWORD_MODEL, NULL); +} + +static GQueue * +search_single_book (DhBook *book, + SearchSettings *settings, + guint max_hits, + DhLink **exact_link) +{ + GQueue *ret; + GList *l; + + ret = g_queue_new (); + + for (l = dh_book_get_links (book); + l != NULL && ret->length < max_hits; + l = l->next) { + DhLink *link = l->data; + + if (!_dh_search_context_match_link (settings->search_context, + link, + settings->prefix)) { + continue; + } + + g_queue_push_tail (ret, dh_link_ref (link)); + + if (exact_link == NULL || !settings->prefix) + continue; + + /* Look for an exact link match. If the link is a PAGE, we can + * overwrite any previous exact link set. For example, when + * looking for GFile, we want the page, not the struct. + */ + if ((*exact_link == NULL || dh_link_get_link_type (link) == DH_LINK_TYPE_PAGE) && + _dh_search_context_is_exact_link (settings->search_context, link)) { + *exact_link = link; + } + } + + return ret; +} + +static GQueue * +search_books (SearchSettings *settings, + guint max_hits, + DhLink **exact_link) +{ + DhBookManager *book_manager; + GList *books; + GList *l; + GQueue *ret; + + ret = g_queue_new (); + + book_manager = dh_book_manager_get_singleton (); + books = dh_book_manager_get_books (book_manager); + + for (l = books; + l != NULL && ret->length < max_hits; + l = l->next) { + DhBook *book = DH_BOOK (l->data); + GQueue *book_result; + + if (!_dh_search_context_match_book (settings->search_context, book)) + continue; + + /* Filtering by book? */ + if (settings->book_id != NULL && + g_strcmp0 (settings->book_id, dh_book_get_id (book)) != 0) { + continue; + } + + /* Skipping a given book? */ + if (settings->skip_book_id != NULL && + g_strcmp0 (settings->skip_book_id, dh_book_get_id (book)) == 0) { + continue; + } + + book_result = search_single_book (book, + settings, + max_hits - ret->length, + exact_link); + + dh_util_queue_concat (ret, book_result); + } + + g_queue_sort (ret, (GCompareDataFunc) dh_link_compare, NULL); + return ret; +} + +static GQueue * +handle_book_id_only (DhSearchContext *search_context, + DhLink **exact_link) +{ + DhBookManager *book_manager; + GList *books; + GList *l; + GQueue *ret; + + if (_dh_search_context_get_book_id (search_context) == NULL || + _dh_search_context_get_page_id (search_context) != NULL || + _dh_search_context_get_keywords (search_context) != NULL) { + return NULL; + } + + ret = g_queue_new (); + + book_manager = dh_book_manager_get_singleton (); + books = dh_book_manager_get_books (book_manager); + + for (l = books; l != NULL; l = l->next) { + DhBook *book = DH_BOOK (l->data); + GNode *node; + + if (!_dh_search_context_match_book (search_context, book)) + continue; + + /* Return only the top-level book page. */ + node = dh_book_get_tree (book); + if (node != NULL) { + DhLink *link; + + link = node->data; + g_queue_push_tail (ret, dh_link_ref (link)); + + if (exact_link != NULL) + *exact_link = link; + } + + break; + } + + return ret; +} + +/* The Search rationale is as follows: + * + * - If 'book_id' is given, but no 'page_id' or 'keywords', the main page of + * the book will only be shown, giving as exact match this book link. + * - If 'book_id' and 'page_id' are given, but no 'keywords', all the items + * in the given page of the given book will be shown. + * - If 'book_id' and 'keywords' are given, but no 'page_id', up to MAX_HITS + * items matching the keywords in the given book will be shown. + * - If 'book_id' and 'page_id' and 'keywords' are given, all the items + * matching the keywords in the given page of the given book will be shown. + * + * - If 'page_id' is given, but no 'book_id' or 'keywords', all the items + * in the given page will be shown, giving as exact match the page link. + * - If 'page_id' and 'keywords' are given but no 'book_id', all the items + * matching the keywords in the given page will be shown. + * + * - If 'keywords' only are given, up to max_hits items matching the keywords + * will be shown. If keyword matches both a page link and a non-page one, + * the page link is the one given as exact match. + */ +static GQueue * +keyword_model_search (DhKeywordModel *model, + DhSearchContext *search_context, + DhLink **exact_link) +{ + DhKeywordModelPrivate *priv = dh_keyword_model_get_instance_private (model); + SearchSettings settings; + guint max_hits = MAX_HITS; + GQueue *in_book = NULL; + GQueue *other_books = NULL; + DhLink *in_book_exact_link = NULL; + DhLink *other_books_exact_link = NULL; + GQueue *out; + + out = handle_book_id_only (search_context, exact_link); + if (out != NULL) + return out; + + out = g_queue_new (); + + settings.search_context = search_context; + settings.book_id = priv->current_book_id; + settings.skip_book_id = NULL; + settings.prefix = TRUE; + + if (_dh_search_context_get_page_id (search_context) != NULL) { + /* If filtering per page, increase the maximum number of + * hits. This is due to the fact that a page may have + * more than MAX_HITS keywords, and the page link may be + * the last one in the list, but we always want to get it. + */ + max_hits = G_MAXUINT; + } + + /* First look for prefixed items in the given book id. */ + if (priv->current_book_id != NULL) { + in_book = search_books (&settings, + max_hits, + &in_book_exact_link); + } + + /* Next, always check other books as well, as the exact match may be in + * there. + */ + settings.book_id = NULL; + settings.skip_book_id = priv->current_book_id; + other_books = search_books (&settings, + max_hits, + &other_books_exact_link); + + /* Now that we got prefix searches in current and other books, decide + * which the preferred exact link is. If the exact match is in other + * books, prefer those to the current book. + */ + if (in_book_exact_link != NULL) { + *exact_link = in_book_exact_link; + dh_util_queue_concat (out, in_book); + dh_util_queue_concat (out, other_books); + } else if (other_books_exact_link != NULL) { + *exact_link = other_books_exact_link; + dh_util_queue_concat (out, other_books); + dh_util_queue_concat (out, in_book); + } else { + *exact_link = NULL; + dh_util_queue_concat (out, in_book); + dh_util_queue_concat (out, other_books); + } + + if (out->length >= max_hits) + return out; + + /* Look for non-prefixed matches in current book. */ + settings.prefix = FALSE; + + if (priv->current_book_id != NULL) { + settings.book_id = priv->current_book_id; + settings.skip_book_id = NULL; + + in_book = search_books (&settings, + max_hits - out->length, + NULL); + + dh_util_queue_concat (out, in_book); + if (out->length >= max_hits) + return out; + } + + /* If still room for more items, look for non-prefixed items in other + * books. + */ + settings.book_id = NULL; + settings.skip_book_id = priv->current_book_id; + other_books = search_books (&settings, + max_hits - out->length, + NULL); + dh_util_queue_concat (out, other_books); + + return out; +} + +/** + * dh_keyword_model_filter: + * @model: a #DhKeywordModel. + * @search_string: a search query. + * @current_book_id: (nullable): the ID of the book currently shown, or %NULL. + * @language: (nullable): deprecated, must be %NULL. + * + * Searches in the #DhBookManager the list of #DhLink's that correspond to + * @search_string, and fills the @model with that list (erasing the previous + * content). + * + * Attention, when calling this function the @model needs to be disconnected + * from the #GtkTreeView, because the #GtkTreeModel signals are not emitted, to + * improve the performances (sending a lot of signals is slow) and have a + * simpler implementation. The previous row selection is anyway no longer + * relevant. + * + * Note that there is a maximum number of matches (configured internally). When + * the maximum is reached the search is stopped, to avoid blocking the GUI + * (since this function runs synchronously) if the @search_string contains for + * example only one character. (And it is anyway not very useful to show to the + * user tens of thousands search results). + * + * Returns: (nullable) (transfer none): the #DhLink that matches exactly + * @search_string, or %NULL if no such #DhLink was found within the maximum + * number of matches. + */ +DhLink * +dh_keyword_model_filter (DhKeywordModel *model, + const gchar *search_string, + const gchar *current_book_id, + const gchar *language) +{ + DhKeywordModelPrivate *priv; + DhSearchContext *search_context; + GQueue *new_links = NULL; + DhLink *exact_link = NULL; + + g_return_val_if_fail (DH_IS_KEYWORD_MODEL (model), NULL); + g_return_val_if_fail (search_string != NULL, NULL); + g_return_val_if_fail (language == NULL, NULL); + + priv = dh_keyword_model_get_instance_private (model); + + g_free (priv->current_book_id); + priv->current_book_id = NULL; + + search_context = _dh_search_context_new (search_string); + + if (search_context != NULL) { + const gchar *book_id_in_search_string; + + book_id_in_search_string = _dh_search_context_get_book_id (search_context); + + if (book_id_in_search_string != NULL) + priv->current_book_id = g_strdup (book_id_in_search_string); + else + priv->current_book_id = g_strdup (current_book_id); + + new_links = keyword_model_search (model, search_context, &exact_link); + } + + clear_links (model); + dh_util_queue_concat (&priv->links, new_links); + new_links = NULL; + + /* The content has been modified, change the stamp so that older + * GtkTreeIter's become invalid. + */ + priv->stamp++; + + _dh_search_context_free (search_context); + + /* One hit */ + if (priv->links.length == 1) + return g_queue_peek_head (&priv->links); + + return exact_link; +} diff --git a/src/dh-preferences.c b/src/dh-preferences.c index 0568c50..ffae33b 100644 --- a/src/dh-preferences.c +++ b/src/dh-preferences.c @@ -143,7 +143,7 @@ preferences_bookshelf_find_book (DhPreferences *prefs, -1); /* We may have reached the start of the next language group here */ - if (first && !in_list_book) { + if (exact_found && first && !in_list_book) { *next_iter = loop_iter; *next_found = TRUE; return; @@ -151,7 +151,7 @@ preferences_bookshelf_find_book (DhPreferences *prefs, /* We can compare pointers directly as we're playing with references * of the same object */ - if (exact_iter && + if (exact_iter && exact_found && in_list_book == book) { *exact_iter = loop_iter; *exact_found = TRUE; @@ -160,7 +160,7 @@ preferences_bookshelf_find_book (DhPreferences *prefs, g_object_unref (in_list_book); return; } - } else if (next_iter && + } else if (next_iter && next_found && dh_book_cmp_by_title (in_list_book, book) > 0) { *next_iter = loop_iter; *next_found = TRUE; @@ -223,7 +223,7 @@ preferences_bookshelf_find_language_group (DhPreferences *prefs, continue; } - if (exact_iter && + if (exact_iter && exact_found && g_ascii_strcasecmp (title, language) == 0) { /* Exact match found! */ *exact_iter = loop_iter; @@ -233,7 +233,7 @@ preferences_bookshelf_find_language_group (DhPreferences *prefs, g_free (title); return; } - } else if (next_iter && + } else if (next_iter && next_found && g_ascii_strcasecmp (title, language) > 0) { *next_iter = loop_iter; *next_found = TRUE; diff --git a/src/dh-preferences.c.covscan b/src/dh-preferences.c.covscan new file mode 100644 index 0000000..0568c50 --- /dev/null +++ b/src/dh-preferences.c.covscan @@ -0,0 +1,673 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2004-2008 Imendio AB + * Copyright (C) 2010 Lanedo GmbH + * Copyright (C) 2012 Thomas Bechtold + * + * 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-preferences.h" + +#include + +#include "dh-book.h" +#include "dh-book-manager.h" +#include "dh-util.h" +#include "dh-settings.h" + +static GtkWidget *prefs_dialog = NULL; + +enum { + COLUMN_ENABLED = 0, + COLUMN_TITLE, + COLUMN_BOOK, + COLUMN_WEIGHT, + COLUMN_INCONSISTENT, + N_COLUMNS +}; + +typedef struct { + /* Fonts tab */ + GtkCheckButton *system_fonts_button; + GtkGrid *fonts_grid; + GtkFontButton *variable_font_button; + GtkFontButton *fixed_font_button; + guint use_system_fonts_id; + guint system_var_id; + guint system_fixed_id; + guint var_id; + guint fixed_id; + + /* Book Shelf tab */ + GtkCellRendererToggle *bookshelf_enabled_toggle; + GtkListStore *bookshelf_store; + GtkCheckButton *bookshelf_group_by_language_button; +} DhPreferencesPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (DhPreferences, dh_preferences, GTK_TYPE_DIALOG) + +static void +dh_preferences_response (GtkDialog *dlg, + gint response_id) +{ + gtk_widget_destroy (GTK_WIDGET (dlg)); +} + +static void +dh_preferences_class_init (DhPreferencesClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass); + + dialog_class->response = dh_preferences_response; + + /* Bind class to template */ + gtk_widget_class_set_template_from_resource (widget_class, + "/org/gnome/devhelp/dh-preferences.ui"); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, system_fonts_button); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, fonts_grid); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, variable_font_button); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, fixed_font_button); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, bookshelf_store); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, bookshelf_group_by_language_button); + gtk_widget_class_bind_template_child_private (widget_class, DhPreferences, bookshelf_enabled_toggle); +} + +static void +preferences_bookshelf_clean_store (DhPreferences *prefs) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + + gtk_list_store_clear (priv->bookshelf_store); +} + +/* Tries to find, starting at 'first' (if given): + * - An exact match of the book + * - The book which should be just after our given book: + * - If first is set, the next book must be in the same language group + * as the given book. + * - If first is NOT set, we don't care about language groups as we're + * iterating from the beginning of the list. + * - Both. + */ +static void +preferences_bookshelf_find_book (DhPreferences *prefs, + DhBook *book, + const GtkTreeIter *first, + GtkTreeIter *exact_iter, + gboolean *exact_found, + GtkTreeIter *next_iter, + gboolean *next_found) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + GtkTreeIter loop_iter; + + g_assert ((exact_iter && exact_found) || (next_iter && next_found)); + + /* Reset all flags to not found */ + if (exact_found) + *exact_found = FALSE; + if (next_found) + *next_found = FALSE; + + /* Setup iteration start */ + if (!first) { + /* If no first given, start iterating from the start of the model */ + if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->bookshelf_store), &loop_iter)) { + /* Store is empty, not found */ + return; + } + } else { + loop_iter = *first; + } + + do { + DhBook *in_list_book = NULL; + + gtk_tree_model_get (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter, + COLUMN_BOOK, &in_list_book, + -1); + + /* We may have reached the start of the next language group here */ + if (first && !in_list_book) { + *next_iter = loop_iter; + *next_found = TRUE; + return; + } + + /* We can compare pointers directly as we're playing with references + * of the same object */ + if (exact_iter && + in_list_book == book) { + *exact_iter = loop_iter; + *exact_found = TRUE; + if (!next_iter) { + /* If we were not requested to look for the next one, end here */ + g_object_unref (in_list_book); + return; + } + } else if (next_iter && + dh_book_cmp_by_title (in_list_book, book) > 0) { + *next_iter = loop_iter; + *next_found = TRUE; + g_object_unref (in_list_book); + return; + } + + if (in_list_book) + g_object_unref (in_list_book); + } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter)); +} + +/* Tries to find: + * - An exact match of the language group + * - The language group which should be just after our given language group. + * - Both. + */ +static void +preferences_bookshelf_find_language_group (DhPreferences *prefs, + const gchar *language, + GtkTreeIter *exact_iter, + gboolean *exact_found, + GtkTreeIter *next_iter, + gboolean *next_found) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + GtkTreeIter loop_iter; + + g_assert ((exact_iter && exact_found) || (next_iter && next_found)); + + /* Reset all flags to not found */ + if (exact_found) + *exact_found = FALSE; + if (next_found) + *next_found = FALSE; + + if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter)) { + /* Store is empty, not found */ + return; + } + + do { + DhBook *book = NULL; + gchar *title = NULL; + + /* 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->bookshelf_store), + &loop_iter, + COLUMN_TITLE, &title, + COLUMN_BOOK, &book, + -1); + + /* If we got a book, it's not a language row */ + if (book) { + g_free (title); + g_object_unref (book); + continue; + } + + if (exact_iter && + g_ascii_strcasecmp (title, language) == 0) { + /* Exact match found! */ + *exact_iter = loop_iter; + *exact_found = TRUE; + if (!next_iter) { + /* If we were not requested to look for the next one, end here */ + g_free (title); + return; + } + } else if (next_iter && + 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->bookshelf_store), + &loop_iter)); +} + +static void +preferences_bookshelf_add_book_to_store (DhPreferences *prefs, + DhBook *book, + gboolean group_by_language) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + GtkTreeIter book_iter; + + /* If grouping by language we need to add the language categories */ + if (group_by_language) { + gchar *indented_title; + GtkTreeIter language_iter; + gboolean language_iter_found; + GtkTreeIter next_language_iter; + gboolean next_language_iter_found; + const gchar *language_title; + gboolean first_in_language = FALSE; + + language_title = dh_book_get_language (book); + + /* Look for the proper language group */ + preferences_bookshelf_find_language_group (prefs, + 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_list_store_append (priv->bookshelf_store, + &language_iter); + } else { + gtk_list_store_insert_before (priv->bookshelf_store, + &language_iter, + &next_language_iter); + } + + gtk_list_store_set (priv->bookshelf_store, + &language_iter, + COLUMN_ENABLED, dh_book_get_enabled (book), + COLUMN_TITLE, language_title, + COLUMN_BOOK, NULL, + COLUMN_WEIGHT, PANGO_WEIGHT_BOLD, + COLUMN_INCONSISTENT, FALSE, + -1); + + first_in_language = TRUE; + } + + /* If we got to add first book in a given language group, just append it. */ + if (first_in_language) { + gtk_list_store_insert_after (priv->bookshelf_store, + &book_iter, + &language_iter); + } else { + GtkTreeIter first_book_iter; + GtkTreeIter next_book_iter; + gboolean next_book_iter_found; + gboolean language_inconsistent = FALSE; + gboolean language_enabled = FALSE; + + /* We may need to reset the inconsistent status of the language item */ + gtk_tree_model_get (GTK_TREE_MODEL (priv->bookshelf_store), + &language_iter, + COLUMN_ENABLED, &language_enabled, + COLUMN_INCONSISTENT, &language_inconsistent, + -1); + /* If inconsistent already, do nothing */ + if (!language_inconsistent) { + if (language_enabled != dh_book_get_enabled (book)) { + gtk_list_store_set (priv->bookshelf_store, + &language_iter, + COLUMN_INCONSISTENT, TRUE, + -1); + } + } + + /* The language will have at least one book, so we move iter to it */ + first_book_iter = language_iter; + gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->bookshelf_store), &first_book_iter); + + /* Find next possible book in language group */ + preferences_bookshelf_find_book (prefs, + book, + &first_book_iter, + NULL, + NULL, + &next_book_iter, + &next_book_iter_found); + if (!next_book_iter_found) { + gtk_list_store_append (priv->bookshelf_store, + &book_iter); + } else { + gtk_list_store_insert_before (priv->bookshelf_store, + &book_iter, + &next_book_iter); + } + } + + /* Add new item with indented title */ + indented_title = g_strdup_printf (" %s", dh_book_get_title (book)); + gtk_list_store_set (priv->bookshelf_store, + &book_iter, + COLUMN_ENABLED, dh_book_get_enabled (book), + COLUMN_TITLE, indented_title, + COLUMN_BOOK, book, + COLUMN_WEIGHT, PANGO_WEIGHT_NORMAL, + COLUMN_INCONSISTENT, FALSE, + -1); + g_free (indented_title); + } else { + /* No language grouping, just order by book title */ + GtkTreeIter next_book_iter; + gboolean next_book_iter_found; + + preferences_bookshelf_find_book (prefs, + book, + NULL, + NULL, + NULL, + &next_book_iter, + &next_book_iter_found); + if (!next_book_iter_found) { + gtk_list_store_append (priv->bookshelf_store, + &book_iter); + } else { + gtk_list_store_insert_before (priv->bookshelf_store, + &book_iter, + &next_book_iter); + } + + gtk_list_store_set (priv->bookshelf_store, + &book_iter, + COLUMN_ENABLED, dh_book_get_enabled (book), + COLUMN_TITLE, dh_book_get_title (book), + COLUMN_BOOK, book, + COLUMN_WEIGHT, PANGO_WEIGHT_NORMAL, + -1); + } +} + +static void +preferences_bookshelf_populate_store (DhPreferences *prefs) +{ + DhBookManager *book_manager; + GList *l; + gboolean group_by_language; + + book_manager = dh_book_manager_get_singleton (); + group_by_language = dh_book_manager_get_group_by_language (book_manager); + + /* This list already comes ordered, but we don't care */ + for (l = dh_book_manager_get_books (book_manager); + l; + l = g_list_next (l)) { + preferences_bookshelf_add_book_to_store (prefs, + DH_BOOK (l->data), + group_by_language); + } +} + +static void +preferences_bookshelf_group_by_language_cb (GObject *object, + GParamSpec *pspec, + DhPreferences *prefs) +{ + preferences_bookshelf_clean_store (prefs); + preferences_bookshelf_populate_store (prefs); +} + +static void +preferences_bookshelf_set_language_inconsistent (DhPreferences *prefs, + const gchar *language) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + GtkTreeIter loop_iter; + GtkTreeIter language_iter; + gboolean language_iter_found; + gboolean one_book_enabled = FALSE; + gboolean one_book_disabled = FALSE; + + preferences_bookshelf_find_language_group (prefs, + language, + &language_iter, + &language_iter_found, + NULL, + NULL); + if (!language_iter_found) { + return; + } + + loop_iter = language_iter; + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter)) { + DhBook *book; + gboolean enabled; + + gtk_tree_model_get (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter, + COLUMN_BOOK, &book, + COLUMN_ENABLED, &enabled, + -1); + if (!book) { + /* Reached next language group */ + break; + } + g_object_unref (book); + + if (enabled) + one_book_enabled = TRUE; + else + one_book_disabled = TRUE; + + if (one_book_enabled == one_book_disabled) + break; + } + + /* If at least one book is enabled AND another book is disabled, + * we need to set inconsistent state */ + if (one_book_enabled == one_book_disabled) { + gtk_list_store_set (priv->bookshelf_store, &language_iter, + COLUMN_INCONSISTENT, TRUE, + -1); + return; + } + + gtk_list_store_set (priv->bookshelf_store, &language_iter, + COLUMN_ENABLED, one_book_enabled, + COLUMN_INCONSISTENT, FALSE, + -1); +} + +static void +preferences_bookshelf_book_deleted_cb (DhBookManager *book_manager, + DhBook *book, + DhPreferences *prefs) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + GtkTreeIter exact_iter; + gboolean exact_iter_found; + + preferences_bookshelf_find_book (prefs, + book, + NULL, + &exact_iter, + &exact_iter_found, + NULL, + NULL); + if (exact_iter_found) { + gtk_list_store_remove (priv->bookshelf_store, &exact_iter); + preferences_bookshelf_set_language_inconsistent (prefs, dh_book_get_language (book)); + } +} + +static void +preferences_bookshelf_book_created_cb (DhBookManager *book_manager, + DhBook *book, + DhPreferences *prefs) +{ + gboolean group_by_language; + + group_by_language = dh_book_manager_get_group_by_language (book_manager); + preferences_bookshelf_add_book_to_store (prefs, book, group_by_language); +} + +static void +preferences_bookshelf_tree_selection_toggled_cb (GtkCellRendererToggle *cell_renderer, + gchar *path, + DhPreferences *prefs) +{ + DhPreferencesPrivate *priv = dh_preferences_get_instance_private (prefs); + DhBookManager *book_manager; + GtkTreeIter iter; + + book_manager = dh_book_manager_get_singleton (); + + if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (priv->bookshelf_store), + &iter, + path)) { + gpointer book = NULL; + gboolean enabled; + + gtk_tree_model_get (GTK_TREE_MODEL (priv->bookshelf_store), + &iter, + COLUMN_BOOK, &book, + COLUMN_ENABLED, &enabled, + -1); + + if (book) { + /* Update book conf */ + dh_book_set_enabled (book, !enabled); + + gtk_list_store_set (priv->bookshelf_store, &iter, + COLUMN_ENABLED, !enabled, + -1); + /* Now we need to look for the language group of this item, + * in order to set the inconsistent state if applies */ + if (dh_book_manager_get_group_by_language (book_manager)) { + preferences_bookshelf_set_language_inconsistent (prefs, dh_book_get_language (book)); + } + + } else { + GtkTreeIter loop_iter; + + /* We should only reach this if we are grouping by language */ + g_assert (dh_book_manager_get_group_by_language (book_manager) == TRUE); + + /* Set new status in the language group item */ + gtk_list_store_set (priv->bookshelf_store, &iter, + COLUMN_ENABLED, !enabled, + COLUMN_INCONSISTENT, FALSE, + -1); + + /* And set new status in all books of the same language */ + loop_iter = iter; + while (gtk_tree_model_iter_next (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter)) { + gtk_tree_model_get (GTK_TREE_MODEL (priv->bookshelf_store), + &loop_iter, + COLUMN_BOOK, &book, + -1); + if (!book) { + /* Found next language group, finish */ + return; + } + + /* Update book conf */ + dh_book_set_enabled (book, !enabled); + + gtk_list_store_set (priv->bookshelf_store, + &loop_iter, + COLUMN_ENABLED, !enabled, + -1); + } + } + } +} + +static void +dh_preferences_init (DhPreferences *prefs) +{ + DhPreferencesPrivate *priv; + DhBookManager *book_manager; + DhSettings *settings; + GSettings *settings_fonts; + GSettings *settings_contents; + + priv = dh_preferences_get_instance_private (prefs); + + gtk_widget_init_template (GTK_WIDGET (prefs)); + + book_manager = dh_book_manager_get_singleton (); + + g_signal_connect_object (book_manager, + "book-created", + G_CALLBACK (preferences_bookshelf_book_created_cb), + prefs, + 0); + + g_signal_connect_object (book_manager, + "book-deleted", + G_CALLBACK (preferences_bookshelf_book_deleted_cb), + prefs, + 0); + + g_signal_connect_object (book_manager, + "notify::group-by-language", + G_CALLBACK (preferences_bookshelf_group_by_language_cb), + prefs, + 0); + + /* setup GSettings bindings */ + settings = dh_settings_get_singleton (); + settings_fonts = dh_settings_peek_fonts_settings (settings); + settings_contents = dh_settings_peek_contents_settings (settings); + g_settings_bind (settings_fonts, "use-system-fonts", + priv->system_fonts_button, "active", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings_fonts, "use-system-fonts", + priv->fonts_grid, "sensitive", + G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN); + g_settings_bind (settings_fonts, "fixed-font", + priv->fixed_font_button, "font-name", + G_SETTINGS_BIND_DEFAULT); + g_settings_bind (settings_fonts, "variable-font", + priv->variable_font_button, "font-name", + G_SETTINGS_BIND_DEFAULT); + + g_settings_bind (settings_contents, "group-books-by-language", + priv->bookshelf_group_by_language_button, "active", + G_SETTINGS_BIND_DEFAULT); + + g_signal_connect (priv->bookshelf_enabled_toggle, + "toggled", + G_CALLBACK (preferences_bookshelf_tree_selection_toggled_cb), + prefs); + + preferences_bookshelf_populate_store (prefs); +} + +void +dh_preferences_show_dialog (GtkWindow *parent) +{ + g_return_if_fail (GTK_IS_WINDOW (parent)); + + if (prefs_dialog == NULL) { + prefs_dialog = GTK_WIDGET (g_object_new (DH_TYPE_PREFERENCES, + "use-header-bar", 1, + NULL)); + g_signal_connect (prefs_dialog, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &prefs_dialog); + } + + if (parent != gtk_window_get_transient_for (GTK_WINDOW (prefs_dialog))) { + gtk_window_set_transient_for (GTK_WINDOW (prefs_dialog), + parent); + gtk_window_set_destroy_with_parent (GTK_WINDOW (prefs_dialog), TRUE); + } + + gtk_window_present (GTK_WINDOW (prefs_dialog)); +}