ephy-completion-model.c 18.2 KB
Newer Older
1
/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
Michael Catanzaro's avatar
Michael Catanzaro committed
2
/*
3
 *  Copyright © 2012 Igalia S.L.
4
 *
Michael Catanzaro's avatar
Michael Catanzaro committed
5 6 7
 *  This file is part of Epiphany.
 *
 *  Epiphany is free software: you can redistribute it and/or modify
8
 *  it under the terms of the GNU General Public License as published by
Michael Catanzaro's avatar
Michael Catanzaro committed
9
 *  the Free Software Foundation, either version 3 of the License, or
10 11
 *  (at your option) any later version.
 *
Michael Catanzaro's avatar
Michael Catanzaro committed
12
 *  Epiphany is distributed in the hope that it will be useful,
13 14 15 16 17
 *  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
Michael Catanzaro's avatar
Michael Catanzaro committed
18
 *  along with Epiphany.  If not, see <http://www.gnu.org/licenses/>.
19 20
 */

21
#include "config.h"
22
#include "ephy-completion-model.h"
23

24
#include "ephy-embed-prefs.h"
25
#include "ephy-embed-shell.h"
26
#include "ephy-favicon-helpers.h"
Xan Lopez's avatar
Xan Lopez committed
27
#include "ephy-history-service.h"
28
#include "ephy-shell.h"
29
#include "ephy-uri-helpers.h"
30 31

#include <string.h>
32

33 34 35
enum {
  PROP_0,
  PROP_HISTORY_SERVICE,
36
  PROP_BOOKMARKS_MANAGER,
37
  LAST_PROP
38 39
};

40 41
struct _EphyCompletionModel {
  GtkListStore parent_instance;
42

Xan Lopez's avatar
Xan Lopez committed
43
  EphyHistoryService *history_service;
44
  GCancellable *cancellable;
45

46
  EphyBookmarksManager *bookmarks_manager;
47
  GSList *search_terms;
48 49
};

50 51 52 53
static GParamSpec *obj_properties[LAST_PROP];

G_DEFINE_TYPE (EphyCompletionModel, ephy_completion_model, GTK_TYPE_LIST_STORE)

54
static void
55
ephy_completion_model_constructed (GObject *object)
56
{
57 58 59
  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 };
60

61 62
  G_OBJECT_CLASS (ephy_completion_model_parent_class)->constructed (object);

63 64 65
  gtk_list_store_set_column_types (GTK_LIST_STORE (object),
                                   N_COL,
                                   types);
66 67 68
}

static void
69 70 71
free_search_terms (GSList *search_terms)
{
  GSList *iter;
Michael Catanzaro's avatar
Michael Catanzaro committed
72

73
  for (iter = search_terms; iter != NULL; iter = iter->next)
Michael Catanzaro's avatar
Michael Catanzaro committed
74 75
    g_regex_unref ((GRegex *)iter->data);

76
  g_slist_free (search_terms);
77
}
78

79 80 81 82 83 84
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) {
Michael Catanzaro's avatar
Michael Catanzaro committed
85 86 87
    case PROP_HISTORY_SERVICE:
      self->history_service = EPHY_HISTORY_SERVICE (g_value_get_pointer (value));
      break;
88 89 90
    case PROP_BOOKMARKS_MANAGER:
      self->bookmarks_manager = EPHY_BOOKMARKS_MANAGER (g_value_get_object (value));
      break;
Michael Catanzaro's avatar
Michael Catanzaro committed
91 92 93
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (self, property_id, pspec);
      break;
94 95 96
  }
}

97
static void
98
ephy_completion_model_finalize (GObject *object)
99
{
100
  EphyCompletionModel *model = EPHY_COMPLETION_MODEL (object);
101

102 103 104
  if (model->search_terms) {
    free_search_terms (model->search_terms);
    model->search_terms = NULL;
105
  }
106

107 108 109
  if (model->cancellable) {
    g_cancellable_cancel (model->cancellable);
    g_clear_object (&model->cancellable);
110 111
  }

112
  G_OBJECT_CLASS (ephy_completion_model_parent_class)->finalize (object);
113 114 115
}

static void
116
ephy_completion_model_class_init (EphyCompletionModelClass *klass)
117
{
118
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
119

120
  object_class->set_property = ephy_completion_model_set_property;
121 122
  object_class->constructed = ephy_completion_model_constructed;
  object_class->finalize = ephy_completion_model_finalize;
123

124 125 126 127 128 129
  obj_properties[PROP_HISTORY_SERVICE] =
    g_param_spec_pointer ("history-service",
                          "History Service",
                          "The history service",
                          G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);

130 131 132 133 134 135
  obj_properties[PROP_BOOKMARKS_MANAGER] =
    g_param_spec_object ("bookmarks-manager",
                         "Bookmarks manager",
                         "The bookmarks manager",
                         EPHY_TYPE_BOOKMARKS_MANAGER,
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
136 137

  g_object_class_install_properties (object_class, LAST_PROP, obj_properties);
138 139 140
}

static void
141
ephy_completion_model_init (EphyCompletionModel *model)
142 143 144
{
}

145 146
static gboolean
is_base_address (const char *address)
147
{
148 149 150 151 152 153 154
  if (address == NULL)
    return FALSE;

  /* A base address is <scheme>://<host>/
   * Neither scheme nor host contain a slash, so we can use slashes
   * figure out if it's a base address.
   *
Michael Catanzaro's avatar
Michael Catanzaro committed
155
   * Note: previous code was using a GRegExp to do the same thing.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
   * 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;
171 172 173
}

static int
174
get_relevance (const char *location,
Michael Catanzaro's avatar
Michael Catanzaro committed
175 176
               int         visit_count,
               gboolean    is_bookmark)
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
{
  /* 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;
  }
Michael Catanzaro's avatar
Michael Catanzaro committed
193

194
  return relevance;
195 196
}

197 198 199 200 201 202 203
typedef struct {
  char *title;
  char *location;
  char *keywords;
  int relevance;
  gboolean is_bookmark;
} PotentialRow;
204

205 206 207 208 209 210 211 212 213
typedef struct {
  GtkListStore *model;
  GtkTreeRowReference *row_reference;
} IconLoadData;

static void
icon_loaded_cb (GObject *source, GAsyncResult *result, gpointer user_data)
{
  GtkTreeIter iter;
214
  GtkTreePath *path;
Michael Catanzaro's avatar
Michael Catanzaro committed
215
  IconLoadData *data = (IconLoadData *)user_data;
216 217 218
  WebKitFaviconDatabase *database = WEBKIT_FAVICON_DATABASE (source);
  GdkPixbuf *favicon = NULL;
  cairo_surface_t *icon_surface = webkit_favicon_database_get_favicon_finish (database, result, NULL);
219

220 221 222 223
  if (icon_surface) {
    favicon = ephy_pixbuf_get_from_surface_scaled (icon_surface, FAVICON_SIZE, FAVICON_SIZE);
    cairo_surface_destroy (icon_surface);
  }
224 225 226 227

  if (favicon) {
    /* The completion model might have changed its contents */
    if (gtk_tree_row_reference_valid (data->row_reference)) {
228 229
      path = gtk_tree_row_reference_get_path (data->row_reference);
      gtk_tree_model_get_iter (GTK_TREE_MODEL (data->model), &iter, path);
230
      gtk_list_store_set (data->model, &iter, EPHY_COMPLETION_FAVICON_COL, favicon, -1);
231 232
      g_object_unref (favicon);
      gtk_tree_path_free (path);
233 234 235 236 237 238 239 240
    }
  }

  g_object_unref (data->model);
  gtk_tree_row_reference_free (data->row_reference);
  g_slice_free (IconLoadData, data);
}

241
static void
242
set_row_in_model (EphyCompletionModel *model, int position, PotentialRow *row)
243
{
244 245 246
  GtkTreeIter iter;
  GtkTreePath *path;
  IconLoadData *data;
Michael Catanzaro's avatar
Michael Catanzaro committed
247
  WebKitFaviconDatabase *database;
248
  EphyEmbedShell *shell = ephy_embed_shell_get_default ();
249

250
  database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (shell));
251

252
  gtk_list_store_insert_with_values (GTK_LIST_STORE (model), &iter, position,
253
                                     EPHY_COMPLETION_TEXT_COL, row->title ? row->title : "",
254 255 256 257 258 259
                                     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);
260 261

  data = g_slice_new (IconLoadData);
Michael Catanzaro's avatar
Michael Catanzaro committed
262
  data->model = GTK_LIST_STORE (g_object_ref (model));
263 264 265 266
  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);

267 268
  webkit_favicon_database_get_favicon (database, row->location,
                                       NULL, icon_loaded_cb, data);
269 270 271
}

static void
272
replace_rows_in_model (EphyCompletionModel *model, GSList *new_rows)
273
{
274 275 276
  /* This is by far the simplest way of doing, and yet it gives
   * basically the same result than the other methods... */
  int i;
277

278
  gtk_list_store_clear (GTK_LIST_STORE (model));
279

280 281 282
  if (!new_rows)
    return;

283
  for (i = 0; new_rows != NULL; i++) {
Michael Catanzaro's avatar
Michael Catanzaro committed
284
    PotentialRow *row = (PotentialRow *)new_rows->data;
285

286
    set_row_in_model (model, i, row);
287 288
    new_rows = new_rows->next;
  }
289
}
290

291
static gboolean
292
should_add_bookmark_to_model (EphyCompletionModel *model,
Michael Catanzaro's avatar
Michael Catanzaro committed
293
                              const char          *search_string,
294
                              EphyBookmark        *bookmark)
295 296
{
  gboolean ret = TRUE;
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
  GSequence *tags;
  GSequenceIter *tag_iter;
  const char *url;
  const char *title;
  char *tag_string = NULL;
  char **tag_array;
  int i;

  title = ephy_bookmark_get_title (bookmark);
  url = ephy_bookmark_get_url (bookmark);
  tags = ephy_bookmark_get_tags (bookmark);

  tag_array = g_malloc0 ((g_sequence_get_length (tags) + 1) * sizeof (char *));

  for (i = 0, tag_iter = g_sequence_get_begin_iter (tags);
       !g_sequence_iter_is_end (tag_iter);
       i++, tag_iter = g_sequence_iter_next (tag_iter)) {
    tag_array[i] = g_sequence_get (tag_iter);
  }

  tag_string = g_strjoinv (" ", tag_array);
318

319
  if (model->search_terms) {
320 321 322
    GSList *iter;
    GRegex *current = NULL;

323
    for (iter = model->search_terms; iter != NULL; iter = iter->next) {
Michael Catanzaro's avatar
Michael Catanzaro committed
324
      current = (GRegex *)iter->data;
325

326
      if ((!g_regex_match (current, title ? title : "", G_REGEX_MATCH_NOTEMPTY, NULL)) &&
327 328
          (!g_regex_match (current, url ? url : "", G_REGEX_MATCH_NOTEMPTY, NULL)) &&
          (!g_regex_match (current, tag_string ? tag_string : "", G_REGEX_MATCH_NOTEMPTY, NULL))) {
329 330 331 332 333 334
        ret = FALSE;
        break;
      }
    }
  }

335 336 337
  g_free (tag_array);
  g_free (tag_string);

338
  return ret;
339 340
}

341 342 343 344 345 346
typedef struct {
  EphyCompletionModel *model;
  char *search_string;
  EphyHistoryJobCallback callback;
  gpointer user_data;
} FindURLsData;
347

348 349 350
static int
find_url (gconstpointer a,
          gconstpointer b)
351
{
Michael Catanzaro's avatar
Michael Catanzaro committed
352
  return g_strcmp0 (((PotentialRow *)a)->location,
353
                    ((char *)b));
354 355
}

356 357 358 359
static PotentialRow *
potential_row_new (const char *title, const char *location,
                   const char *keywords, int visit_count,
                   gboolean is_bookmark)
360
{
361
  PotentialRow *row = g_slice_new0 (PotentialRow);
362

363 364 365 366 367
  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;
368

369
  return row;
370 371
}

372 373
static void
free_potential_row (PotentialRow *row)
374
{
375 376 377
  g_free (row->title);
  g_free (row->location);
  g_free (row->keywords);
Michael Catanzaro's avatar
Michael Catanzaro committed
378

379
  g_slice_free (PotentialRow, row);
380 381
}

382
static GSList *
Michael Catanzaro's avatar
Michael Catanzaro committed
383
add_to_potential_rows (GSList     *rows,
384 385 386
                       const char *title,
                       const char *location,
                       const char *keywords,
Michael Catanzaro's avatar
Michael Catanzaro committed
387 388 389
                       int         visit_count,
                       gboolean    is_bookmark,
                       gboolean    search_for_duplicates)
390 391 392 393 394 395 396 397 398
{
  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) {
Michael Catanzaro's avatar
Michael Catanzaro committed
399
      PotentialRow *match = (PotentialRow *)p->data;
400 401
      if (row->relevance > match->relevance)
        match->relevance = row->relevance;
Michael Catanzaro's avatar
Michael Catanzaro committed
402

403 404 405 406 407 408 409 410 411
      found = TRUE;
      free_potential_row (row);
    }
  }

  if (!found)
    rows = g_slist_prepend (rows, row);

  return rows;
412 413 414
}

static int
415
sort_by_relevance (gconstpointer a, gconstpointer b)
416
{
Michael Catanzaro's avatar
Michael Catanzaro committed
417 418
  PotentialRow *r1 = (PotentialRow *)a;
  PotentialRow *r2 = (PotentialRow *)b;
419

420 421 422 423 424 425
  if (r1->relevance < r2->relevance)
    return 1;
  else if (r1->relevance > r2->relevance)
    return -1;
  else
    return 0;
426 427
}

428 429
static void
query_completed_cb (EphyHistoryService *service,
Michael Catanzaro's avatar
Michael Catanzaro committed
430 431 432
                    gboolean            success,
                    gpointer            result_data,
                    FindURLsData       *user_data)
433 434 435
{
  EphyCompletionModel *model = user_data->model;
  GList *p, *urls;
436 437
  GSequence *bookmarks;
  GSequenceIter *iter;
438 439 440
  GSList *list = NULL;

  /* Bookmarks */
441
  bookmarks = ephy_bookmarks_manager_get_bookmarks (model->bookmarks_manager);
442 443 444 445

  /* 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. */
446 447 448 449 450 451 452 453
  for (iter = g_sequence_get_begin_iter (bookmarks);
       !g_sequence_iter_is_end (iter);
       iter = g_sequence_iter_next (iter)) {
    EphyBookmark *bookmark;
    const char *url, *title;

    bookmark = g_sequence_get (iter);

454 455 456
    if (should_add_bookmark_to_model (model, user_data->search_string, bookmark)) {
      url = ephy_bookmark_get_url (bookmark);
      title = ephy_bookmark_get_title (bookmark);
457
      list = add_to_potential_rows (list, title, url, NULL, 0, TRUE, FALSE);
458
    }
459 460 461
  }

  /* History */
Michael Catanzaro's avatar
Michael Catanzaro committed
462
  urls = (GList *)result_data;
463 464

  for (p = urls; p != NULL; p = p->next) {
Michael Catanzaro's avatar
Michael Catanzaro committed
465
    EphyHistoryURL *url = (EphyHistoryURL *)p->data;
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482

    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);
483
  g_list_free_full (urls, (GDestroyNotify)ephy_history_url_free);
484
  g_slist_free_full (list, (GDestroyNotify)free_potential_row);
485
  g_clear_object (&model->cancellable);
486
}
487

488 489
static void
update_search_terms (EphyCompletionModel *model,
Michael Catanzaro's avatar
Michael Catanzaro committed
490
                     const char          *text)
491 492 493 494 495 496 497 498 499 500
{
  const char *current;
  const char *ptr;
  char *tmp;
  char *term;
  GRegex *term_regex;
  GRegex *quote_regex;
  gint count;
  gboolean inside_quotes = FALSE;

501 502 503
  if (model->search_terms) {
    free_search_terms (model->search_terms);
    model->search_terms = NULL;
504 505 506 507
  }

  quote_regex = g_regex_new ("\"", G_REGEX_OPTIMIZE,
                             G_REGEX_MATCH_NOTEMPTY, NULL);
Michael Catanzaro's avatar
Michael Catanzaro committed
508

509 510 511 512 513 514 515 516
  /*
   * 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++) {
    /*
Michael Catanzaro's avatar
Michael Catanzaro committed
517
     * If we found a double quote character; we will
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
     * 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++;
Michael Catanzaro's avatar
Michael Catanzaro committed
539

540 541 542 543 544 545 546 547 548 549 550 551 552 553 554
      /*
       * 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);
555
        model->search_terms = g_slist_append (model->search_terms, term_regex);
556 557 558 559 560 561 562 563 564 565
      }
      g_free (term);

      /* count will be incremented by the for loop */
      count = -1;
      current = ptr + 1;
    }
  }

  g_regex_unref (quote_regex);
566 567
}

568 569 570
#define MAX_COMPLETION_HISTORY_URLS 8

void
Michael Catanzaro's avatar
Michael Catanzaro committed
571 572
ephy_completion_model_update_for_string (EphyCompletionModel   *model,
                                         const char            *search_string,
573
                                         EphyHistoryJobCallback callback,
Michael Catanzaro's avatar
Michael Catanzaro committed
574
                                         gpointer               data)
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597
{
  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);

  /* 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;

598 599 600
  if (model->cancellable) {
    g_cancellable_cancel (model->cancellable);
    g_object_unref (model->cancellable);
601
  }
602
  model->cancellable = g_cancellable_new ();
603

604
  ephy_history_service_find_urls (model->history_service,
Xan Lopez's avatar
Xan Lopez committed
605
                                  0, 0,
606
                                  MAX_COMPLETION_HISTORY_URLS, 0,
607 608
                                  query,
                                  EPHY_HISTORY_SORT_MOST_VISITED,
609
                                  model->cancellable,
Xan Lopez's avatar
Xan Lopez committed
610 611
                                  (EphyHistoryJobCallback)query_completed_cb,
                                  user_data);
612 613
}

614
EphyCompletionModel *
615 616
ephy_completion_model_new (EphyHistoryService   *history_service,
                           EphyBookmarksManager *bookmarks_manager)
617
{
618
  g_return_val_if_fail (EPHY_IS_HISTORY_SERVICE (history_service), NULL);
619
  g_return_val_if_fail (EPHY_IS_BOOKMARKS_MANAGER (bookmarks_manager), NULL);
620 621 622

  return g_object_new (EPHY_TYPE_COMPLETION_MODEL,
                       "history-service", history_service,
623
                       "bookmarks-manager", bookmarks_manager,
624
                       NULL);
625
}