/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* * Copyright © 2012 Igalia S.L. * * 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 "ephy-completion-model.h" #include "ephy-embed-prefs.h" #include "ephy-embed-shell.h" #include "ephy-favicon-helpers.h" #include "ephy-history-service.h" #include "ephy-shell.h" #include "ephy-uri-helpers.h" #include enum { PROP_0, PROP_HISTORY_SERVICE, PROP_BOOKMARKS, PROP_USE_MARKUP }; G_DEFINE_TYPE (EphyCompletionModel, ephy_completion_model, GTK_TYPE_LIST_STORE) #define EPHY_COMPLETION_MODEL_GET_PRIVATE(object)(G_TYPE_INSTANCE_GET_PRIVATE ((object), EPHY_TYPE_COMPLETION_MODEL, EphyCompletionModelPrivate)) struct _EphyCompletionModelPrivate { EphyHistoryService *history_service; GCancellable *cancellable; EphyNode *bookmarks; EphyNode *smart_bookmarks; GSList *search_terms; gboolean use_markup; }; static void ephy_completion_model_constructed (GObject *object) { GType types[N_COL] = { G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_INT, G_TYPE_STRING, G_TYPE_BOOLEAN, GDK_TYPE_PIXBUF }; gtk_list_store_set_column_types (GTK_LIST_STORE (object), N_COL, types); } static void free_search_terms (GSList *search_terms) { GSList *iter; for (iter = search_terms; iter != NULL; iter = iter->next) g_regex_unref ((GRegex*)iter->data); g_slist_free (search_terms); } static void ephy_completion_model_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { EphyCompletionModel *self = EPHY_COMPLETION_MODEL (object); switch (property_id) { case PROP_HISTORY_SERVICE: self->priv->history_service = EPHY_HISTORY_SERVICE (g_value_get_pointer (value)); break; case PROP_BOOKMARKS: { EphyBookmarks *bookmarks = EPHY_BOOKMARKS (g_value_get_pointer (value)); self->priv->bookmarks = ephy_bookmarks_get_bookmarks (bookmarks); self->priv->smart_bookmarks = ephy_bookmarks_get_smart_bookmarks (bookmarks); } break; case PROP_USE_MARKUP: self->priv->use_markup = g_value_get_boolean (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec); break; } } static void ephy_completion_model_finalize (GObject *object) { EphyCompletionModelPrivate *priv = EPHY_COMPLETION_MODEL (object)->priv; if (priv->search_terms) { free_search_terms (priv->search_terms); priv->search_terms = NULL; } if (priv->cancellable) { g_cancellable_cancel (priv->cancellable); g_clear_object (&priv->cancellable); } G_OBJECT_CLASS (ephy_completion_model_parent_class)->finalize (object); } static void ephy_completion_model_class_init (EphyCompletionModelClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->set_property = ephy_completion_model_set_property; object_class->constructed = ephy_completion_model_constructed; object_class->finalize = ephy_completion_model_finalize; g_object_class_install_property (object_class, PROP_HISTORY_SERVICE, g_param_spec_pointer ("history-service", "History Service", "The history service", G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); g_object_class_install_property (object_class, PROP_BOOKMARKS, g_param_spec_pointer ("bookmarks", "Bookmarks", "The bookmarks", G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); g_object_class_install_property (object_class, PROP_USE_MARKUP, g_param_spec_boolean ("use-markup", "Whether we should be using markup", "Whether we should be using markup", TRUE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK | G_PARAM_STATIC_BLURB)); g_type_class_add_private (object_class, sizeof (EphyCompletionModelPrivate)); } static void ephy_completion_model_init (EphyCompletionModel *model) { model->priv = EPHY_COMPLETION_MODEL_GET_PRIVATE (model); } static gboolean is_base_address (const char *address) { if (address == NULL) return FALSE; /* A base address is :/// * Neither scheme nor host contain a slash, so we can use slashes * figure out if it's a base address. * * Note: previous code was using a GRegExp to do the same thing. * While regexps are much nicer to read, they're also a lot * slower. */ address = strchr (address, '/'); if (address == NULL || address[1] != '/') return FALSE; address += 2; address = strchr (address, '/'); if (address == NULL || address[1] != 0) return FALSE; return TRUE; } static int get_relevance (const char *location, int visit_count, gboolean is_bookmark) { /* FIXME: use frecency. */ int relevance = 0; /* We have three ordered groups: history's base addresses, bookmarks, deep history addresses. */ if (is_bookmark) relevance = 1 << 5; else { visit_count = MIN (visit_count, (1 << 5) - 1); if (is_base_address (location)) relevance = visit_count << 10; else relevance = visit_count; } return relevance; } typedef struct { char *title; char *location; char *keywords; int relevance; gboolean is_bookmark; } PotentialRow; typedef struct { GtkListStore *model; GtkTreeRowReference *row_reference; } IconLoadData; static void icon_loaded_cb (GObject *source, GAsyncResult *result, gpointer user_data) { GtkTreeIter iter; GtkTreePath *path; IconLoadData *data = (IconLoadData *) user_data; WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source); GdkPixbuf *favicon = NULL; cairo_surface_t *icon_surface = webkit_favicon_database_get_favicon_finish (database, result, NULL); if (icon_surface) { favicon = ephy_pixbuf_get_from_surface_scaled (icon_surface, FAVICON_SIZE, FAVICON_SIZE); cairo_surface_destroy (icon_surface); } if (favicon) { /* The completion model might have changed its contents */ if (gtk_tree_row_reference_valid (data->row_reference)) { path = gtk_tree_row_reference_get_path (data->row_reference); gtk_tree_model_get_iter (GTK_TREE_MODEL (data->model), &iter, path); gtk_list_store_set (data->model, &iter, EPHY_COMPLETION_FAVICON_COL, favicon, -1); g_object_unref (favicon); gtk_tree_path_free (path); } } g_object_unref (data->model); gtk_tree_row_reference_free (data->row_reference); g_slice_free (IconLoadData, data); } static gchar * get_row_text (const gchar *url, const gchar *title, const gchar *subtitle_color) { gchar *unescaped_url; gchar *text; if (!url) return g_markup_escape_text (title, -1); unescaped_url = ephy_uri_safe_unescape (url); if (g_strcmp0 (url, title) == 0) text = g_markup_escape_text (unescaped_url, -1); else text = g_markup_printf_escaped ("%s\n%s", title, subtitle_color, unescaped_url); g_free (unescaped_url); return text; } static void set_row_in_model (EphyCompletionModel *model, int position, PotentialRow *row, const gchar *subtitle_color) { GtkTreeIter iter; GtkTreePath *path; IconLoadData *data; WebKitFaviconDatabase* database; gchar *text; EphyEmbedShell *shell = ephy_embed_shell_get_default (); database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (shell)); if (model->priv->use_markup) text = get_row_text (row->location, row->title, subtitle_color); else text = g_strdup (row->title); gtk_list_store_insert_with_values (GTK_LIST_STORE (model), &iter, position, EPHY_COMPLETION_TEXT_COL, text ? text : "", EPHY_COMPLETION_URL_COL, row->location, EPHY_COMPLETION_ACTION_COL, row->location, EPHY_COMPLETION_KEYWORDS_COL, row->keywords ? row->keywords : "", EPHY_COMPLETION_EXTRA_COL, row->is_bookmark, EPHY_COMPLETION_RELEVANCE_COL, row->relevance, -1); g_free (text); data = g_slice_new (IconLoadData); data->model = GTK_LIST_STORE (g_object_ref(model)); path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter); data->row_reference = gtk_tree_row_reference_new (GTK_TREE_MODEL (model), path); gtk_tree_path_free (path); webkit_favicon_database_get_favicon (database, row->location, NULL, icon_loaded_cb, data); } static gchar * get_text_column_subtitle_color (void) { GtkWidgetPath *path; GtkStyleContext *style_context; GdkRGBA rgba; path = gtk_widget_path_new (); gtk_widget_path_prepend_type (path, GTK_TYPE_ENTRY); gtk_widget_path_iter_add_class (path, 0, GTK_STYLE_CLASS_ENTRY); style_context = gtk_style_context_new (); gtk_style_context_set_path (style_context, path); gtk_widget_path_free (path); gtk_style_context_add_class (style_context, GTK_STYLE_CLASS_ENTRY); gtk_style_context_get_color (style_context, GTK_STATE_FLAG_INSENSITIVE, &rgba); g_object_unref (style_context); return g_strdup_printf ("#%04X%04X%04X", (guint)(rgba.red * (gdouble)65535), (guint)(rgba.green * (gdouble)65535), (guint)(rgba.blue * (gdouble)65535)); } static void replace_rows_in_model (EphyCompletionModel *model, GSList *new_rows) { /* This is by far the simplest way of doing, and yet it gives * basically the same result than the other methods... */ int i; gchar *subtitle_color = NULL; gtk_list_store_clear (GTK_LIST_STORE (model)); if (!new_rows) return; if (model->priv->use_markup) subtitle_color = get_text_column_subtitle_color (); for (i = 0; new_rows != NULL; i++) { PotentialRow *row = (PotentialRow*)new_rows->data; set_row_in_model (model, i, row, subtitle_color); new_rows = new_rows->next; } g_free (subtitle_color); } static gboolean should_add_bookmark_to_model (EphyCompletionModel *model, const char *search_string, const char *title, const char *location, const char *keywords) { gboolean ret = TRUE; EphyCompletionModelPrivate *priv = model->priv; if (priv->search_terms) { GSList *iter; GRegex *current = NULL; for (iter = priv->search_terms; iter != NULL; iter = iter->next) { current = (GRegex*)iter->data; if ((!g_regex_match (current, title ? title : "", G_REGEX_MATCH_NOTEMPTY, NULL)) && (!g_regex_match (current, location ? location : "", G_REGEX_MATCH_NOTEMPTY, NULL)) && (!g_regex_match (current, keywords ? keywords : "", G_REGEX_MATCH_NOTEMPTY, NULL))) { ret = FALSE; break; } } } return ret; } typedef struct { EphyCompletionModel *model; char *search_string; EphyHistoryJobCallback callback; gpointer user_data; } FindURLsData; static int find_url (gconstpointer a, gconstpointer b) { return g_strcmp0 (((PotentialRow*)a)->location, ((char *)b)); } static PotentialRow * potential_row_new (const char *title, const char *location, const char *keywords, int visit_count, gboolean is_bookmark) { PotentialRow *row = g_slice_new0 (PotentialRow); row->title = g_strdup (title); row->location = g_strdup (location); row->keywords = g_strdup (keywords); row->relevance = get_relevance (location, visit_count, is_bookmark); row->is_bookmark = is_bookmark; return row; } static void free_potential_row (PotentialRow *row) { g_free (row->title); g_free (row->location); g_free (row->keywords); g_slice_free (PotentialRow, row); } static GSList * add_to_potential_rows (GSList *rows, const char *title, const char *location, const char *keywords, int visit_count, gboolean is_bookmark, gboolean search_for_duplicates) { gboolean found = FALSE; PotentialRow *row = potential_row_new (title, location, keywords, visit_count, is_bookmark); if (search_for_duplicates) { GSList *p; p = g_slist_find_custom (rows, location, find_url); if (p) { PotentialRow *match = (PotentialRow*)p->data; if (row->relevance > match->relevance) match->relevance = row->relevance; found = TRUE; free_potential_row (row); } } if (!found) rows = g_slist_prepend (rows, row); return rows; } static int sort_by_relevance (gconstpointer a, gconstpointer b) { PotentialRow *r1 = (PotentialRow*)a; PotentialRow *r2 = (PotentialRow*)b; if (r1->relevance < r2->relevance) return 1; else if (r1->relevance > r2->relevance) return -1; else return 0; } static void query_completed_cb (EphyHistoryService *service, gboolean success, gpointer result_data, FindURLsData *user_data) { EphyCompletionModel *model = user_data->model; EphyCompletionModelPrivate *priv = model->priv; GList *p, *urls; GPtrArray *children; GSList *list = NULL; int i; /* Bookmarks */ children = ephy_node_get_children (priv->bookmarks); /* FIXME: perhaps this could be done in a service thread? There * should never be a ton of bookmarks, but seems a bit cleaner and * consistent with what we do for the history. */ for (i = 0; i < children->len; i++) { EphyNode *kid; const char *keywords, *location, *title; gboolean is_smart; kid = g_ptr_array_index (children, i); location = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_LOCATION); title = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_TITLE); keywords = ephy_node_get_property_string (kid, EPHY_NODE_BMK_PROP_KEYWORDS); is_smart = ephy_node_has_child (priv->smart_bookmarks, kid); /* Smart bookmarks are already added to the completion menu as completion actions */ if (!is_smart && should_add_bookmark_to_model (model, user_data->search_string, title, location, keywords)) list = add_to_potential_rows (list, title, location, keywords, 0, TRUE, FALSE); } /* History */ urls = (GList*)result_data; for (p = urls; p != NULL; p = p->next) { EphyHistoryURL *url = (EphyHistoryURL*)p->data; list = add_to_potential_rows (list, url->title, url->url, NULL, url->visit_count, FALSE, TRUE); } /* Sort the rows by relevance. */ list = g_slist_sort (list, sort_by_relevance); /* Now that we have all the rows we want to insert, replace the rows * in the current model one by one, sorted by relevance. */ replace_rows_in_model (model, list); /* Notify */ if (user_data->callback) user_data->callback (service, success, result_data, user_data->user_data); g_free (user_data->search_string); g_slice_free (FindURLsData, user_data); g_list_free_full (urls, (GDestroyNotify)ephy_history_url_free); g_slist_free_full (list, (GDestroyNotify)free_potential_row); g_clear_object (&priv->cancellable); } static void update_search_terms (EphyCompletionModel *model, const char *text) { const char *current; const char *ptr; char *tmp; char *term; GRegex *term_regex; GRegex *quote_regex; gint count; gboolean inside_quotes = FALSE; EphyCompletionModelPrivate *priv = model->priv; if (priv->search_terms) { free_search_terms (priv->search_terms); priv->search_terms = NULL; } quote_regex = g_regex_new ("\"", G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL); /* * This code loops through the string using pointer arythmetics. * Although the string we are handling may contain UTF-8 chars * this works because only ASCII chars affect what is actually * copied from the string as a search term. */ for (count = 0, current = ptr = text; ptr[0] != '\0'; ptr++, count++) { /* * If we found a double quote character; we will * consume bytes up until the next quote, or * end of line; */ if (ptr[0] == '"') inside_quotes = !inside_quotes; /* * If we found a space, and we are not looking for a * closing double quote, or if the next char is the * end of the string, append what we have already as * a search term. */ if (((ptr[0] == ' ') && (!inside_quotes)) || ptr[1] == '\0') { /* * We special-case the end of the line because * we would otherwise not copy the last character * of the search string, since the for loop will * stop before that. */ if (ptr[1] == '\0') count++; /* * remove quotes, and quote any regex-sensitive * characters */ tmp = g_regex_escape_string (current, count); term = g_regex_replace (quote_regex, tmp, -1, 0, "", G_REGEX_MATCH_NOTEMPTY, NULL); g_strstrip (term); g_free (tmp); /* we don't want empty search terms */ if (term[0] != '\0') { term_regex = g_regex_new (term, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, G_REGEX_MATCH_NOTEMPTY, NULL); priv->search_terms = g_slist_append (priv->search_terms, term_regex); } g_free (term); /* count will be incremented by the for loop */ count = -1; current = ptr + 1; } } g_regex_unref (quote_regex); } #define MAX_COMPLETION_HISTORY_URLS 8 void ephy_completion_model_update_for_string (EphyCompletionModel *model, const char *search_string, EphyHistoryJobCallback callback, gpointer data) { EphyCompletionModelPrivate *priv; char **strings; int i; GList *query = NULL; FindURLsData *user_data; g_return_if_fail (EPHY_IS_COMPLETION_MODEL (model)); g_return_if_fail (search_string != NULL); priv = model->priv; /* Split the search string. */ strings = g_strsplit (search_string, " ", -1); for (i = 0; strings[i]; i++) query = g_list_append (query, g_strdup (strings[i])); g_strfreev (strings); update_search_terms (model, search_string); user_data = g_slice_new (FindURLsData); user_data->model = model; user_data->search_string = g_strdup (search_string); user_data->callback = callback; user_data->user_data = data; if (priv->cancellable) { g_cancellable_cancel (priv->cancellable); g_object_unref (priv->cancellable); } priv->cancellable = g_cancellable_new (); ephy_history_service_find_urls (priv->history_service, 0, 0, MAX_COMPLETION_HISTORY_URLS, 0, query, EPHY_HISTORY_SORT_MOST_VISITED, priv->cancellable, (EphyHistoryJobCallback)query_completed_cb, user_data); } EphyCompletionModel * ephy_completion_model_new (EphyHistoryService *history_service, EphyBookmarks *bookmarks, gboolean use_markup) { g_return_val_if_fail (EPHY_IS_HISTORY_SERVICE (history_service), NULL); g_return_val_if_fail (EPHY_IS_BOOKMARKS (bookmarks), NULL); return g_object_new (EPHY_TYPE_COMPLETION_MODEL, "history-service", history_service, "bookmarks", bookmarks, "use-markup", use_markup, NULL); }