[epiphany] search-engines: Port whole EphySearchEngineManager code to GListModel



commit 33179c39c5e5e20d625ead2cabcd8c8d01a825af
Author: vanadiae <vanadiae35 gmail com>
Date:   Sat Jan 29 15:16:49 2022 +0100

    search-engines: Port whole EphySearchEngineManager code to GListModel
    
    This commit ports EphySearchEngineManager to being a proper GListModel,
    with a separate EphySearchEngine object for each search engines. That makes
    the code overall way cleaner and easier to work with, as there's no need
    to keep track of the old_name and saved_name or whatever horrible thing
    that I needed to do, since now we just need to keep around the EphySearchEngine
    object we're displaying in this preferences row, and do changes on it directly.
    That did require quite a few changes all around the code base to adapt to all
    API changes, but it is definitely worth it.
    
    Part-of: <https://gitlab.gnome.org/GNOME/epiphany/-/merge_requests/1055>

 embed/ephy-embed-utils.c                     |  88 +--
 lib/ephy-search-engine-manager.c             | 795 ++++++++++++++++-----------
 lib/ephy-search-engine-manager.h             |  51 +-
 lib/ephy-search-engine.c                     | 244 ++++++++
 lib/ephy-search-engine.h                     |  51 ++
 lib/meson.build                              |   1 +
 src/ephy-suggestion-model.c                  |  27 +-
 src/ephy-window.c                            |   7 +-
 src/preferences/ephy-prefs-dialog.c          |  11 +-
 src/preferences/ephy-search-engine-listbox.c | 383 ++++++++++---
 src/preferences/ephy-search-engine-listbox.h |   5 -
 src/preferences/ephy-search-engine-row.c     | 287 +++-------
 src/preferences/ephy-search-engine-row.h     |   6 +-
 src/resources/gtk/search-engine-listbox.ui   |  14 -
 src/resources/gtk/search-engine-row.ui       |   2 +-
 src/search-provider/ephy-search-provider.c   |  17 +-
 tests/ephy-web-view-test.c                   |  28 +-
 17 files changed, 1262 insertions(+), 755 deletions(-)
---
diff --git a/embed/ephy-embed-utils.c b/embed/ephy-embed-utils.c
index c3a915984..a21319f31 100644
--- a/embed/ephy-embed-utils.c
+++ b/embed/ephy-embed-utils.c
@@ -25,8 +25,10 @@
 #include "ephy-embed-utils.h"
 
 #include "ephy-about-handler.h"
+#include "ephy-embed-shell.h"
 #include "ephy-prefs.h"
 #include "ephy-reader-handler.h"
+#include "ephy-search-engine-manager.h"
 #include "ephy-settings.h"
 #include "ephy-string.h"
 #include "ephy-view-source-handler.h"
@@ -197,34 +199,6 @@ is_public_domain (const char *address)
   return retval;
 }
 
-static gboolean
-is_bang_search (const char *address)
-{
-  EphyEmbedShell *shell;
-  EphySearchEngineManager *search_engine_manager;
-  char **bangs;
-  GString *buffer;
-
-  shell = ephy_embed_shell_get_default ();
-  search_engine_manager = ephy_embed_shell_get_search_engine_manager (shell);
-  bangs = ephy_search_engine_manager_get_bangs (search_engine_manager);
-
-  for (uint i = 0; bangs[i] != NULL; i++) {
-    buffer = g_string_new (bangs[i]);
-    g_string_append (buffer, " ");
-
-    if (strstr (address, buffer->str) == address) {
-      g_string_free (buffer, TRUE);
-      g_free (bangs);
-      return TRUE;
-    }
-    g_string_free (buffer, TRUE);
-  }
-  g_free (bangs);
-
-  return FALSE;
-}
-
 static gboolean
 is_host_with_port (const char *address)
 {
@@ -241,6 +215,13 @@ is_host_with_port (const char *address)
   return port != 0;
 }
 
+/* This function checks whether @address can point to a web page.
+ * It accepts as potential sources for web page not only full URI/URLs, but also:
+ * - local absolute file path
+ * - IP address
+ * - host:port
+ * - localhost
+ */
 gboolean
 ephy_embed_utils_address_is_valid (const char *address)
 {
@@ -262,7 +243,6 @@ ephy_embed_utils_address_is_valid (const char *address)
            ephy_embed_utils_address_is_existing_absolute_filename (address) ||
            g_regex_match (get_non_search_regex (), address, 0, NULL) ||
            is_public_domain (address) ||
-           is_bang_search (address) ||
            is_host_with_port (address);
 
   g_clear_object (&info);
@@ -287,6 +267,11 @@ ensure_host_name_is_lowercase (const char *address)
     return g_strdup (address);
 }
 
+/* Does various normalization rules to make sure @input_address ends up
+ * with a URI scheme (e.g. absolute filenames or "localhost"), changes
+ * the URI scheme to something more appropriate when needed and lowercases
+ * the hostname.
+ */
 char *
 ephy_embed_utils_normalize_address (const char *input_address)
 {
@@ -294,19 +279,6 @@ ephy_embed_utils_normalize_address (const char *input_address)
   g_autofree gchar *address = NULL;
 
   g_assert (input_address);
-  /* We don't want to lowercase the host name if it's a bang search, as it's not a URI.
-   * It would otherwise lowercase the entire search string, bang included, which is not
-   * what we want. So use input_address directly.
-   */
-  if (is_bang_search (input_address)) {
-    EphyEmbedShell *shell;
-    EphySearchEngineManager *search_engine_manager;
-
-    shell = ephy_embed_shell_get_default ();
-    search_engine_manager = ephy_embed_shell_get_search_engine_manager (shell);
-    return ephy_search_engine_manager_parse_bang_search (search_engine_manager,
-                                                         input_address);
-  }
 
   address = ensure_host_name_is_lowercase (input_address);
 
@@ -342,38 +314,34 @@ ephy_embed_utils_normalize_address (const char *input_address)
   return effective_address ? effective_address : g_strdup (address);
 }
 
+/* Searches @search_key with the default search engine. */
 char *
 ephy_embed_utils_autosearch_address (const char *search_key)
 {
-  char *query_param;
-  const char *address_search;
-  char *effective_address;
   EphyEmbedShell *shell;
-  EphySearchEngineManager *search_engine_manager;
+  EphySearchEngineManager *manager;
+  EphySearchEngine *engine;
 
   if (!g_settings_get_boolean (EPHY_SETTINGS_WEB, EPHY_PREFS_WEB_ENABLE_AUTOSEARCH))
     return g_strdup (search_key);
 
   shell = ephy_embed_shell_get_default ();
-  search_engine_manager = ephy_embed_shell_get_search_engine_manager (shell);
-  address_search = ephy_search_engine_manager_get_default_search_address (search_engine_manager);
-
-  query_param = soup_form_encode ("q", search_key, NULL);
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wformat-nonliteral"
-  /* Format string under control of user input... but gsettings is trusted input. */
-  /* + 2 here is getting rid of 'q=' */
-  effective_address = g_strdup_printf (address_search, query_param + 2);
-#pragma GCC diagnostic pop
-  g_free (query_param);
-
-  return effective_address;
+  manager = ephy_embed_shell_get_search_engine_manager (shell);
+  engine = ephy_search_engine_manager_get_default_engine (manager);
+  g_assert (engine != NULL);
+
+  return ephy_search_engine_build_search_address (engine, search_key);
 }
 
 char *
 ephy_embed_utils_normalize_or_autosearch_address (const char *address)
 {
-  if (ephy_embed_utils_address_is_valid (address))
+  EphySearchEngineManager *manager = ephy_embed_shell_get_search_engine_manager 
(ephy_embed_shell_get_default ());
+  char *bang_search = ephy_search_engine_manager_parse_bang_search (manager, address);
+
+  if (bang_search)
+    return bang_search;
+  else if (ephy_embed_utils_address_is_valid (address))
     return ephy_embed_utils_normalize_address (address);
   else
     return ephy_embed_utils_autosearch_address (address);
diff --git a/lib/ephy-search-engine-manager.c b/lib/ephy-search-engine-manager.c
index 96285a721..3729090d2 100644
--- a/lib/ephy-search-engine-manager.c
+++ b/lib/ephy-search-engine-manager.c
@@ -1,6 +1,7 @@
 /* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /*
  *  Copyright © 2017 Cedric Le Moigne <cedlemo gmx com>
+ *  Copyright 2021 vanadiae <vanadiae35 gmail com>
  *
  *  This file is part of Epiphany.
  *
@@ -19,6 +20,7 @@
  */
 
 #include "config.h"
+
 #include "ephy-search-engine-manager.h"
 
 #include "ephy-file-helpers.h"
@@ -27,437 +29,612 @@
 #include "ephy-settings.h"
 #include "ephy-prefs.h"
 
-#include <libsoup/soup.h>
-
-#define FALLBACK_ADDRESS "https://duckduckgo.com/?q=%s&t=epiphany";
+struct _EphySearchEngineManager {
+  GObject parent_instance;
 
-enum {
-  SEARCH_ENGINES_CHANGED,
-  LAST_SIGNAL
-};
+  GPtrArray *engines;
 
-static guint signals[LAST_SIGNAL];
+  EphySearchEngine *default_engine; /* unowned */
 
-struct _EphySearchEngineManager {
-  GObject parent_instance;
-  GHashTable *search_engines;
+  /* This is just to speed things up. It updates based on each search engine's
+   * notify::bang signal, so it is never out of sync because signal callbacks
+   * are called synchronously. The key is the bang, and the value is the
+   * corresponding EphySearchEngine.
+   */
+  GHashTable *bangs;
 };
 
-typedef struct {
-  char *address;
-  char *bang;
-} EphySearchEngineInfo;
+static void list_model_iface_init (GListModelInterface *iface,
+                                   gpointer             iface_data);
 
-G_DEFINE_TYPE (EphySearchEngineManager, ephy_search_engine_manager, G_TYPE_OBJECT)
+G_DEFINE_TYPE_WITH_CODE (EphySearchEngineManager, ephy_search_engine_manager,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
 
-static void
-ephy_search_engine_info_free (EphySearchEngineInfo *info)
-{
-  g_free (info->address);
-  g_free (info->bang);
-  g_free (info);
-}
+enum {
+  PROP_0,
+  PROP_DEFAULT_ENGINE,
+  N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
 
-static EphySearchEngineInfo *
-ephy_search_engine_info_new (const char *address,
-                             const char *bang)
+static int
+search_engine_compare_func (EphySearchEngine **a,
+                            EphySearchEngine **b)
 {
-  EphySearchEngineInfo *info;
-  info = g_malloc (sizeof (EphySearchEngineInfo));
-  info->address = g_strdup (address);
-  info->bang = g_strdup (bang);
-  return info;
+  return g_strcmp0 (ephy_search_engine_get_name (*a),
+                    ephy_search_engine_get_name (*b));
 }
 
 static void
-search_engines_changed_cb (GSettings *settings,
-                           char      *key,
-                           gpointer   user_data)
+on_search_engine_bang_changed_cb (EphySearchEngine        *engine,
+                                  GParamSpec              *pspec,
+                                  EphySearchEngineManager *manager)
 {
-  g_signal_emit (EPHY_SEARCH_ENGINE_MANAGER (user_data),
-                 signals[SEARCH_ENGINES_CHANGED], 0);
+  GHashTableIter iter;
+  const char *bang;
+  EphySearchEngine *old_bang_engine;
+
+  g_hash_table_iter_init (&iter, manager->bangs);
+
+  /* We have no way of knowing what bang @engine was previously using, so
+   * we must iterate the whole bangs hash table to find @engine, remove its
+   * bang-engine pair and finally insert it back with its new bang.
+   */
+  while (g_hash_table_iter_next (&iter, (gpointer *)&bang, (gpointer *)&old_bang_engine)) {
+    if (old_bang_engine == engine) {
+      /* We found the engine by its pointer (not bang), so we remove it from the hash table. */
+      g_hash_table_iter_remove (&iter);
+    }
+  }
+
+  bang = ephy_search_engine_get_bang (engine);
+
+  /* Now that we've removed the engine from the hash table (with its old bang),
+   * we can add it back with its new value, in case its bang isn't empty.
+   */
+  if (*bang != '\0')
+    g_hash_table_insert (manager->bangs, (gpointer)bang, engine);
 }
 
 static void
-ephy_search_engine_manager_init (EphySearchEngineManager *manager)
+load_search_engines_from_settings (EphySearchEngineManager *manager)
 {
   g_autoptr (GVariantIter) iter = NULL;
-  GVariant *search_engine;
-
-  manager->search_engines = g_hash_table_new_full (g_str_hash,
-                                                   g_str_equal,
-                                                   g_free,
-                                                   (GDestroyNotify)ephy_search_engine_info_free);
+  GVariant *variant;
+  g_autofree char *default_engine_name = g_settings_get_string (EPHY_SETTINGS_MAIN, 
EPHY_PREFS_DEFAULT_SEARCH_ENGINE);
 
   g_settings_get (EPHY_SETTINGS_MAIN, EPHY_PREFS_SEARCH_ENGINES, "aa{sv}", &iter);
 
-  while ((search_engine = g_variant_iter_next_value (iter))) {
-    const char *address;
-    const char *bang;
-    char *name = NULL;
+  while ((variant = g_variant_iter_next_value (iter))) {
     GVariantDict dict;
-    EphySearchEngineInfo *info;
+    const char *name, *url, *bang;
+    g_autoptr (EphySearchEngine) search_engine = NULL;
+
+    g_variant_dict_init (&dict, variant);
+
+    /* All of those checks are just to make sure we keep our state clean and
+     * respect the non-NULL expectations.
+     */
+    if (!g_variant_dict_lookup (&dict, "name", "&s", &name))
+      name = "";
+    if (!g_variant_dict_lookup (&dict, "url", "&s", &url))
+      url = "";
+    if (!g_variant_dict_lookup (&dict, "bang", "&s", &bang))
+      bang = "";
+
+    search_engine = g_object_new (EPHY_TYPE_SEARCH_ENGINE,
+                                  "name", name,
+                                  "url", url,
+                                  "bang", bang,
+                                  NULL);
+    g_assert (EPHY_IS_SEARCH_ENGINE (search_engine));
+
+    /* Bangs are assumed to be unique, so this shouldn't happen unless GSettings
+     * are wrongly modified or we messed up input validation in the UI.
+     */
+    if (g_hash_table_lookup (manager->bangs, bang)) {
+      g_warning ("Found bang %s assigned to several search engines in GSettings."
+                 "The bang for %s is hence reset to avoid collision.",
+                 bang, name);
+      ephy_search_engine_set_bang (search_engine, "");
+    }
 
-    g_variant_dict_init (&dict, search_engine);
-    g_variant_dict_lookup (&dict, "url", "&s", &address);
-    g_variant_dict_lookup (&dict, "bang", "&s", &bang);
-    g_variant_dict_lookup (&dict, "name", "s", &name);
+    ephy_search_engine_manager_add_engine (manager, search_engine);
+    if (g_strcmp0 (ephy_search_engine_get_name (search_engine), default_engine_name) == 0)
+      ephy_search_engine_manager_set_default_engine (manager, search_engine);
 
-    info = ephy_search_engine_info_new (address, bang);
+    g_variant_unref (variant);
+  }
 
-    g_hash_table_insert (manager->search_engines, name, info);
+  /* Both of these conditions should never actually be encountered, unless someone
+   * messed up with GSettings manually or we did something wrong in the UI
+   * (i.e. validation code has an issue in the prefs).
+   */
+  if (G_UNLIKELY (manager->engines->len == 0)) {
+    g_settings_reset (EPHY_SETTINGS_MAIN, EPHY_PREFS_SEARCH_ENGINES);
+    g_settings_reset (EPHY_SETTINGS_MAIN, EPHY_PREFS_DEFAULT_SEARCH_ENGINE);
+    load_search_engines_from_settings (manager);
 
-    g_variant_unref (search_engine);
+    g_warning ("Having no search engine is forbidden. Resetting to default ones instead.");
   }
+  g_assert (manager->engines->len > 0);
 
-  g_signal_connect (EPHY_SETTINGS_MAIN,
-                    "changed::search-engines",
-                    G_CALLBACK (search_engines_changed_cb), manager);
+  if (G_UNLIKELY (!manager->default_engine)) {
+    g_warning ("Could not find default search engine set in the gsettings within all available search 
engines! Setting the first one as fallback.");
+    ephy_search_engine_manager_set_default_engine (manager, manager->engines->pdata[0]);
+  }
 }
 
 static void
-ephy_search_engine_manager_dispose (GObject *object)
+ephy_search_engine_manager_init (EphySearchEngineManager *manager)
 {
-  EphySearchEngineManager *manager = EPHY_SEARCH_ENGINE_MANAGER (object);
+  /* We don't use _new_full(), as we'll directly insert unowned bangs from
+   * ephy_search_engine_get_bang(), and the value belongs to us anyway (as part
+   * of the list store), so both don't need to be freed.
+   */
+  manager->bangs = g_hash_table_new (g_str_hash, g_str_equal);
 
-  g_clear_pointer (&manager->search_engines, g_hash_table_destroy);
+  manager->engines = g_ptr_array_new_with_free_func (g_object_unref);
 
-  G_OBJECT_CLASS (ephy_search_engine_manager_parent_class)->dispose (object);
+  load_search_engines_from_settings (manager);
 }
 
 static void
-ephy_search_engine_manager_class_init (EphySearchEngineManagerClass *klass)
+ephy_search_engine_manager_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->dispose = ephy_search_engine_manager_dispose;
-
-  signals[SEARCH_ENGINES_CHANGED] = g_signal_new ("changed",
-                                                  EPHY_TYPE_SEARCH_ENGINE_MANAGER,
-                                                  G_SIGNAL_RUN_LAST,
-                                                  0,
-                                                  NULL, NULL, NULL,
-                                                  G_TYPE_NONE, 0);
+  EphySearchEngineManager *self = EPHY_SEARCH_ENGINE_MANAGER (object);
+
+  switch (prop_id) {
+    case PROP_DEFAULT_ENGINE:
+      g_value_take_object (value, ephy_search_engine_manager_get_default_engine (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
 }
 
-EphySearchEngineManager *
-ephy_search_engine_manager_new (void)
+static void
+ephy_search_engine_manager_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
 {
-  return EPHY_SEARCH_ENGINE_MANAGER (g_object_new (EPHY_TYPE_SEARCH_ENGINE_MANAGER, NULL));
+  EphySearchEngineManager *self = EPHY_SEARCH_ENGINE_MANAGER (object);
+
+  switch (prop_id) {
+    case PROP_DEFAULT_ENGINE:
+      ephy_search_engine_manager_set_default_engine (self, g_value_get_object (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
 }
 
-const char *
-ephy_search_engine_manager_get_address (EphySearchEngineManager *manager,
-                                        const char              *name)
+static void
+ephy_search_engine_manager_finalize (GObject *object)
 {
-  EphySearchEngineInfo *info;
-
-  info = (EphySearchEngineInfo *)g_hash_table_lookup (manager->search_engines, name);
+  EphySearchEngineManager *manager = EPHY_SEARCH_ENGINE_MANAGER (object);
 
-  if (info)
-    return info->address;
+  g_clear_pointer (&manager->bangs, g_hash_table_destroy);
+  g_clear_pointer (&manager->engines, g_ptr_array_unref);
 
-  return NULL;
+  G_OBJECT_CLASS (ephy_search_engine_manager_parent_class)->finalize (object);
 }
 
-const char *
-ephy_search_engine_manager_get_default_search_address (EphySearchEngineManager *manager)
+static void
+ephy_search_engine_manager_class_init (EphySearchEngineManagerClass *klass)
 {
-  char *name;
-  const char *address;
-
-  name = ephy_search_engine_manager_get_default_engine (manager);
-  address = ephy_search_engine_manager_get_address (manager, name);
-  g_free (name);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
-  return address ? address : FALLBACK_ADDRESS;
+  object_class->finalize = ephy_search_engine_manager_finalize;
+  object_class->get_property = ephy_search_engine_manager_get_property;
+  object_class->set_property = ephy_search_engine_manager_set_property;
+
+  properties [PROP_DEFAULT_ENGINE] =
+    g_param_spec_object ("default-engine",
+                         "Default search engine",
+                         "The default search engine for this manager.",
+                         EPHY_TYPE_SEARCH_ENGINE,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS |
+                          G_PARAM_EXPLICIT_NOTIFY));
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
-const char *
-ephy_search_engine_manager_get_bang (EphySearchEngineManager *manager,
-                                     const char              *name)
+static GType
+list_model_get_item_type (GListModel *list)
 {
-  EphySearchEngineInfo *info;
-
-  info = (EphySearchEngineInfo *)g_hash_table_lookup (manager->search_engines, name);
-
-  if (info)
-    return info->bang;
-
-  return NULL;
+  return EPHY_TYPE_SEARCH_ENGINE;
 }
 
-char *
-ephy_search_engine_manager_get_default_engine (EphySearchEngineManager *manager)
+static guint
+list_model_get_n_items (GListModel *list)
 {
-  return g_settings_get_string (EPHY_SETTINGS_MAIN, EPHY_PREFS_DEFAULT_SEARCH_ENGINE);
+  EphySearchEngineManager *manager = EPHY_SEARCH_ENGINE_MANAGER (list);
+
+  return manager->engines->len;
 }
 
-gboolean
-ephy_search_engine_manager_set_default_engine (EphySearchEngineManager *manager,
-                                               const char              *name)
+static gpointer
+list_model_get_item (GListModel *list,
+                     guint       position)
 {
-  if (!g_hash_table_contains (manager->search_engines, name))
-    return FALSE;
+  EphySearchEngineManager *manager = EPHY_SEARCH_ENGINE_MANAGER (list);
 
-  return g_settings_set_string (EPHY_SETTINGS_MAIN, EPHY_PREFS_DEFAULT_SEARCH_ENGINE, name);
+  if (position >= manager->engines->len)
+    return NULL;
+  else
+    return g_object_ref (manager->engines->pdata[position]);
 }
 
-char **
-ephy_search_engine_manager_get_names (EphySearchEngineManager *manager)
+static void
+list_model_iface_init (GListModelInterface *iface,
+                       gpointer             iface_data)
 {
-  GHashTableIter iter;
-  gpointer key;
-  char **search_engine_names;
-  guint size;
-  guint i = 0;
-
-  size = g_hash_table_size (manager->search_engines);
-  search_engine_names = g_new0 (char *, size + 1);
-
-  g_hash_table_iter_init (&iter, manager->search_engines);
-
-  while (g_hash_table_iter_next (&iter, &key, NULL))
-    search_engine_names[i++] = g_strdup ((char *)key);
+  iface->get_item_type = list_model_get_item_type;
+  iface->get_n_items = list_model_get_n_items;
+  iface->get_item = list_model_get_item;
+}
 
-  return search_engine_names;
+EphySearchEngineManager *
+ephy_search_engine_manager_new (void)
+{
+  return EPHY_SEARCH_ENGINE_MANAGER (g_object_new (EPHY_TYPE_SEARCH_ENGINE_MANAGER, NULL));
 }
 
 /**
- * ephy_search_engine_manager_engine_exists:
+ * ephy_search_engine_manager_get_default_engine:
  *
- * Checks if search engine @name exists in @manager.
- *
- * @manager: the #EphySearchEngineManager
- * @name:    the name of the search engine
- *
- * Returns: %TRUE if the search engine was found, %FALSE otherwise.
+ * Returns: (transfer none): the default search engine for @manager.
  */
-gboolean
-ephy_search_engine_manager_engine_exists (EphySearchEngineManager *manager,
-                                          const char              *name)
-{
-  return !!g_hash_table_lookup (manager->search_engines, name);
-}
-
-char **
-ephy_search_engine_manager_get_bangs (EphySearchEngineManager *manager)
+EphySearchEngine *
+ephy_search_engine_manager_get_default_engine (EphySearchEngineManager *manager)
 {
-  GHashTableIter iter;
-  gpointer value;
-  char **search_engine_bangs;
-  guint size;
-  guint i = 0;
-
-  size = g_hash_table_size (manager->search_engines);
-  search_engine_bangs = g_new0 (char *, size + 1);
-
-  g_hash_table_iter_init (&iter, manager->search_engines);
+  g_assert (EPHY_IS_SEARCH_ENGINE (manager->default_engine));
 
-  while (g_hash_table_iter_next (&iter, NULL, &value))
-    search_engine_bangs[i++] = ((EphySearchEngineInfo *)value)->bang;
-
-  return search_engine_bangs;
+  return manager->default_engine;
 }
 
-static void
-ephy_search_engine_manager_apply_settings (EphySearchEngineManager *manager)
+/**
+ * ephy_search_engine_manager_set_default_engine:
+ * @engine: (transfer none): the search engine to set as default for @manager.
+ *   This search engine must already be added to the search engine manager.
+ *
+ * Note that you must call ephy_search_engine_manager_save_to_settings() when
+ * appropriate to save it. It isn't done automatically because we don't save
+ * the search engines themselves on every change, as that would be pretty expensive
+ * when typing the information, so it's better if the default search engine and
+ * the search engines themselves are always kept in sync, in case there's an issue
+ * somewhere in the code where it doesn't save one part or another.
+ */
+void
+ephy_search_engine_manager_set_default_engine (EphySearchEngineManager *manager,
+                                               EphySearchEngine        *engine)
 {
-  GHashTableIter iter;
-  EphySearchEngineInfo *info;
-  gpointer key;
-  gpointer value;
-  GVariantBuilder builder;
-  GVariant *variant;
+  g_assert (EPHY_IS_SEARCH_ENGINE (engine));
+  /* Improper input validation if that happens in our code. */
+  g_assert (g_ptr_array_find (manager->engines, engine, NULL));
 
-  g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
-  g_hash_table_iter_init (&iter, manager->search_engines);
-
-  while (g_hash_table_iter_next (&iter, &key, &value)) {
-    GVariantDict dict;
-
-    info = (EphySearchEngineInfo *)value;
-    g_assert (key != NULL);
-    g_assert (info != NULL);
-    g_assert (info->address != NULL);
-    g_assert (info->bang != NULL);
-
-    g_variant_dict_init (&dict, NULL);
-    g_variant_dict_insert (&dict, "url", "s", info->address);
-    g_variant_dict_insert (&dict, "bang", "s", info->bang);
-    g_variant_dict_insert (&dict, "name", "s", key);
-
-    g_variant_builder_add_value (&builder, g_variant_dict_end (&dict));
-  }
-  variant = g_variant_builder_end (&builder);
-  g_settings_set_value (EPHY_SETTINGS_MAIN, EPHY_PREFS_SEARCH_ENGINES, variant);
+  manager->default_engine = engine;
+  g_object_notify_by_pspec (G_OBJECT (manager), properties[PROP_DEFAULT_ENGINE]);
 }
 
+/**
+ * ephy_search_engine_manager_add_engine:
+ * @engine: The search engine to add to @manager. @manager will take a reference
+ *   on it.
+ *
+ * Adds search engine @engine to @manager.
+ */
 void
 ephy_search_engine_manager_add_engine (EphySearchEngineManager *manager,
-                                       const char              *name,
-                                       const char              *address,
-                                       const char              *bang)
+                                       EphySearchEngine        *engine)
 {
-  EphySearchEngineInfo *info;
+  gboolean bang_existed = FALSE;
+  guint new_sorted_position;
 
-  info = ephy_search_engine_info_new (address, bang);
-  g_hash_table_insert (manager->search_engines, g_strdup (name), info);
-  ephy_search_engine_manager_apply_settings (manager);
+  if (*ephy_search_engine_get_bang (engine) != '\0') {
+    bang_existed = !g_hash_table_insert (manager->bangs,
+                                         (gpointer)ephy_search_engine_get_bang (engine),
+                                         engine);
+  }
+  /* Programmer/validation error that doesn't properly use ephy_search_engine_manager_has_bang(). */
+  g_assert (!bang_existed);
+  g_signal_connect (engine, "notify::bang", G_CALLBACK (on_search_engine_bang_changed_cb), manager);
+
+  g_ptr_array_add (manager->engines, g_object_ref (engine));
+
+  /* It's a pity there isn't a more efficient g_ptr_array_add_sorted() function.
+   * Comparison should be fast anyway, but still.
+   */
+  g_ptr_array_sort (manager->engines, (GCompareFunc)search_engine_compare_func);
+
+  /* The engine likely will have moved in the array so we need to make sure
+   * to report the items-changed signal at the proper position.
+   */
+  g_assert (g_ptr_array_find (manager->engines, engine, &new_sorted_position));
+  g_list_model_items_changed (G_LIST_MODEL (manager),
+                              new_sorted_position,
+                              0,
+                              1);
 }
 
 void
 ephy_search_engine_manager_delete_engine (EphySearchEngineManager *manager,
-                                          const char              *name)
+                                          EphySearchEngine        *engine)
 {
-  g_hash_table_remove (manager->search_engines, name);
-  ephy_search_engine_manager_apply_settings (manager);
+  guint pos;
+  const char *bang;
+
+  /* Never allow not having a search engine, as too much relies on having one
+   * and it just doesn't make sense at all to not have one. We assert as the
+   * validation should prevent this from happening, so if it crashes then it's
+   * for a good reason and the code should be fixed.
+   */
+  g_assert (manager->engines->len > 1);
+
+  /* Removing an engine not in the manager is a programmer error. */
+  g_assert (g_ptr_array_find (manager->engines, engine, &pos));
+
+  bang = ephy_search_engine_get_bang (engine);
+  if (*bang != '\0')
+    g_hash_table_remove (manager->bangs, bang);
+
+  /* Temporary ref so that we can remove the engine, and be sure that
+   * the engine at index 0 isn't already the same as this one when
+   * setting back another engine as default one.
+   */
+  g_object_ref (engine);
+
+  g_ptr_array_remove_index (manager->engines, pos);
+
+  if (engine == manager->default_engine) {
+    g_assert (manager->engines->len != 0);
+
+    /* Just set the first search engine in the sorted array as new search engine
+     * so we're sure we'll still have a valid default search engine at any time.
+     */
+    ephy_search_engine_manager_set_default_engine (manager, manager->engines->pdata[0]);
+  }
+
+  /* Drop temporary ref. */
+  g_object_unref (engine);
+
+  g_list_model_items_changed (G_LIST_MODEL (manager), pos, 1, 0);
 }
 
 /**
- * ephy_search_engine_manager_rename:
- *
- * Renames search engine @old_name to @new_name, taking care of setting it back
- * as default search engine if needed.
+ * ephy_search_engine_manager_find_engine_by_name:
+ * @engine_name: The name of the search engine to look for.
  *
- * @manager: a #EphySearchEngineManager
- * @old_name: the current name of the search engine
- * @new_name: the new name for search engine @old_name
+ * Iterates @manager and finds the first search engine with its name set to @engine_name.
+ * This is just a helper function, it isn't more efficient than iterating @manager
+ * yourself and making string comparison with the engine's name.
  *
- * Returns: %FALSE if there wasn't any renaming to do (if both old and new names
- * were the same), %TRUE if the search engine was renamed.
+ * Returns: (transfer none): The #EphySearchEngine with name @engine_name if found in @manager, or %NULL if 
not found.
  */
-gboolean
-ephy_search_engine_manager_rename (EphySearchEngineManager *manager,
-                                   const char              *old_name,
-                                   const char              *new_name)
+EphySearchEngine *
+ephy_search_engine_manager_find_engine_by_name (EphySearchEngineManager *manager,
+                                                const char              *engine_name)
 {
-  EphySearchEngineInfo *info, *info_copy;
-
-  if (g_strcmp0 (old_name, new_name) == 0)
-    return FALSE;
-
-  info = g_hash_table_lookup (manager->search_engines, old_name);
-  g_assert_nonnull (info);
+  for (guint i = 0; i < manager->engines->len; i++) {
+    EphySearchEngine *engine = manager->engines->pdata[i];
 
-  info_copy = ephy_search_engine_info_new (info->address, info->bang);
-  g_hash_table_remove (manager->search_engines, old_name);
-  g_hash_table_insert (manager->search_engines, g_strdup (new_name), info_copy);
-  /* Set the search engine back as default engine if it was the default one. */
-  if (g_strcmp0 (ephy_search_engine_manager_get_default_engine (manager), old_name) == 0)
-    ephy_search_engine_manager_set_default_engine (manager, new_name);
-  ephy_search_engine_manager_apply_settings (manager);
+    if (g_strcmp0 (ephy_search_engine_get_name (engine), engine_name) == 0)
+      return engine;
+  }
 
-  return TRUE;
+  return NULL;
 }
 
-void
-ephy_search_engine_manager_modify_engine (EphySearchEngineManager *manager,
-                                          const char              *name,
-                                          const char              *address,
-                                          const char              *bang)
+/**
+ * ephy_search_engine_manager_has_bang:
+ * @bang: the bang to look for
+ *
+ * Checks whether @manager has a search engine that uses @bang as shortcut bang.
+ * This is easier and more efficient than iterating manually on @manager yourself
+ * and check for the bang for each search engine, as @manager internally keeps
+ * a hash table with all used bangs.
+ *
+ * Returns: Whether @manager already has a search engine with its bang set to @bang.
+ */
+gboolean
+ephy_search_engine_manager_has_bang (EphySearchEngineManager *manager,
+                                     const char              *bang)
 {
-  EphySearchEngineInfo *info;
-
-  /* You can't modify a non-existant search engine. */
-  g_assert (g_hash_table_contains (manager->search_engines, name));
-
-  info = ephy_search_engine_info_new (address, bang);
-  g_hash_table_replace (manager->search_engines,
-                        g_strdup (name),
-                        info);
-  ephy_search_engine_manager_apply_settings (manager);
+  return g_hash_table_lookup (manager->bangs, bang) != NULL;
 }
 
-const char *
-ephy_search_engine_manager_engine_from_bang (EphySearchEngineManager *manager,
-                                             const char              *bang)
+/**
+ * parse_bang_query:
+ * @search: the search with bangs to perform
+ * @choosen_bang_engine: (out): if this function returns a non %NULL value, this
+ *   argument will be set to the search engine from @manager that should be used
+ *   to perform the search using the search query this function returns.
+ *
+ * This is the implementation for ephy_search_engine_manager_parse_bang_search()
+ * and ephy_search_engine_manager_parse_bang_suggestions(). See the doc of the
+ * former for details on this function's behaviours.
+ *
+ * Returns: (transfer full): the search query without the bangs.
+ */
+static char *
+parse_bang_query (EphySearchEngineManager  *manager,
+                  const char               *search,
+                  EphySearchEngine        **choosen_bang_engine)
 {
-  GHashTableIter iter;
-  EphySearchEngineInfo *info;
-  gpointer key;
-  gpointer value;
+  g_autofree char *first_word = NULL;
+  g_autofree char *last_word = NULL;
+  g_autofree char *query_without_bangs = NULL;
 
-  g_hash_table_iter_init (&iter, manager->search_engines);
+  /* i.e. the end of @last_word */
+  const char *last_non_space_p;
 
-  while (g_hash_table_iter_next (&iter, &key, &value)) {
-    info = (EphySearchEngineInfo *)value;
-    if (g_strcmp0 (bang, info->bang) == 0)
-      return (const char *)key;
-  }
+  /* i.e. the start of @first_word */
+  const char *first_non_space_p;
 
-  return NULL;
-}
+  /* Both of these are set appropriately when we discover each bang within @search. */
+  const char *query_start, *query_end;
 
-static char *
-ephy_search_engine_manager_replace_pattern (const char *string,
-                                            const char *pattern,
-                                            const char *replace)
-{
-  gchar **strings;
-  gchar *query_param;
-  const gchar *escaped_replace;
-  GString *buffer;
+  /* This one is separate from query_{start,end} because e.g. if the last word isn't
+   * a bang, then we'll want to include it so query_end will be last_non_space_p.
+   * Otherwise query_end will be space_p. */
+  const char *space_p;
+  EphySearchEngine *final_bang_engine = NULL, *bang_engine = NULL;
 
-  strings = g_strsplit (string, pattern, -1);
-  query_param = soup_form_encode ("q", replace, NULL);
-  escaped_replace = query_param + 2;
+  g_assert (search != NULL);
+  if (*search == '\0')
+    return NULL;
 
-  buffer = g_string_new (NULL);
+  last_non_space_p = search + strlen (search) - 1;
+  while (last_non_space_p != search && *last_non_space_p == ' ')
+    last_non_space_p = g_utf8_find_prev_char (search, last_non_space_p);
 
-  for (guint i = 0; strings[i] != NULL; i++) {
-    if (i > 0)
-      g_string_append (buffer, escaped_replace);
+  first_non_space_p = search;
+  while (*first_non_space_p == ' ')
+    first_non_space_p = g_utf8_find_next_char (first_non_space_p, NULL);
 
-    g_string_append (buffer, strings[i]);
-  }
+  /* Means the search query is empty or is full of spaces. So not a bang search. */
+  if (last_non_space_p <= first_non_space_p)
+    return NULL;
 
-  g_strfreev (strings);
-  g_free (query_param);
+  /* There's no strrnchr() available, so must backwards iterate ourselves to
+   * find the space character between @last_non_space_p and @search's beginning
+   */
+  space_p = last_non_space_p;
+  while (space_p != search && *space_p != ' ')
+    space_p = g_utf8_find_prev_char (search, space_p);
+
+  /* This is necessary here because @last_non_space_p will point _at_ the
+   * last non space character, not _just after_ it, which is not how substring
+   * lengths are usually calculated like (since g_strndup (first_p, last_p - first_p)
+   * should work without having to use +1 all around).
+   */
+  last_non_space_p++;
+
+  /* There is a word, but only one, so it can't be a proper bang search */
+  if (space_p <= first_non_space_p)
+    return NULL;
 
-  return g_string_free (buffer, FALSE);
-}
+  /* +1 to skip the space. */
+  last_word = g_strndup (space_p + 1, last_non_space_p - (space_p + 1));
+  bang_engine = g_hash_table_lookup (manager->bangs, last_word);
 
-char *
-ephy_search_engine_manager_build_search_address (EphySearchEngineManager *manager,
-                                                 const char              *name,
-                                                 const char              *search)
-{
-  EphySearchEngineInfo *info;
+  /* Don't include the last word in the query as it's a proper bang. */
+  if (bang_engine) {
+    query_end = space_p;
+    final_bang_engine = bang_engine;
+  }
+  /* The last word isn't a bang, so include it in the query. */
+  else {
+    query_end = last_non_space_p;
+  }
 
-  info = (EphySearchEngineInfo *)g_hash_table_lookup (manager->search_engines, name);
+  space_p = strchr (first_non_space_p, ' ');
+  first_word = g_strndup (first_non_space_p, space_p - first_non_space_p);
+  bang_engine = g_hash_table_lookup (manager->bangs, first_word);
+  if (bang_engine) {
+    /* +1 to skip the space. */
+    query_start = space_p + 1;
+
+    /* We prefer using the last typed bang (the one at the end), so that's
+     * what we'll prefer using here.
+     */
+    if (!final_bang_engine)
+      final_bang_engine = bang_engine;
+  } else {
+    /* It's not a proper bang, so we need to include it in the search query. */
+    query_start = first_non_space_p;
+  }
 
-  if (info == NULL)
+  /* No valid bang was found for this search query, so it's not a bang search. */
+  if (!final_bang_engine)
     return NULL;
 
-  return ephy_search_engine_manager_replace_pattern (info->address, "%s", search);
+  /* Now that we've placed query_start and query_end properly depending on
+   * whether the first/last word is a valid bang, we can copy the part that
+   * doesn't include all the bangs to search this query using the search engine
+   * we found for the bang.
+   */
+  query_without_bangs = g_strndup (query_start, query_end - query_start);
+
+  *choosen_bang_engine = final_bang_engine;
+
+  return g_steal_pointer (&query_without_bangs);
 }
 
+/**
+ * ephy_search_engine_manager_parse_bang_search:
+ *
+ * This function looks at the first and last word of @search, checks if
+ * one of them is the bang of one of the search engines in @manager, and
+ * returns the corresponding search URL as returned by ephy_search_engine_build_search_address().
+ * The last word will be looked at first, so that when someone changes their
+ * mind at the end of the line they can just type the new bang and it will
+ * be used instead of the first one.
+ *
+ * What is called a "bang search" is a search of the form "!bang this is the
+ * search query", or with the bang at the end or at both ends (in which case
+ * the end bang will be preferred).
+ *
+ * Returns: (transfer full) (nullable): The search URL corresponding to @search, with
+ *   the search engine picked using the bang available in @search, or %NULL if
+ *   there wasn't any recognized bang engine in @search. As such this function can
+ *   also be used as a way of detecting whether @search is a "bang search", to
+ *   process the search using the default search engine in that case.
+ */
 char *
 ephy_search_engine_manager_parse_bang_search (EphySearchEngineManager *manager,
                                               const char              *search)
 {
-  GHashTableIter iter;
-  EphySearchEngineInfo *info;
-  gpointer value;
-  GString *buffer;
-  char *search_address = NULL;
-
-  g_hash_table_iter_init (&iter, manager->search_engines);
-
-  while (g_hash_table_iter_next (&iter, NULL, &value)) {
-    info = (EphySearchEngineInfo *)value;
-    buffer = g_string_new (info->bang);
-    g_string_append (buffer, " ");
-    if (strstr (search, buffer->str) == search) {
-      search_address = ephy_search_engine_manager_replace_pattern (info->address,
-                                                                   "%s",
-                                                                   (search + buffer->len));
-      g_string_free (buffer, TRUE);
-      return search_address;
-    }
-    g_string_free (buffer, TRUE);
+  EphySearchEngine *engine = NULL;
+  g_autofree char *no_bangs_query = parse_bang_query (manager, search, &engine);
+
+  if (no_bangs_query)
+    return ephy_search_engine_build_search_address (engine, no_bangs_query);
+  else
+    return NULL;
+}
+
+/**
+ * ephy_search_engine_manager_save_to_settings:
+ *
+ * Saves the search engines and the default search engine to the GSettings.
+ *
+ * You must call this function after having done the changes (e.g. when closing
+ * the preferences window).
+ */
+void
+ephy_search_engine_manager_save_to_settings (EphySearchEngineManager *manager)
+{
+  GVariantBuilder builder;
+  GVariant *variant;
+  gpointer item;
+  guint i = 0;
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
+
+  while ((item = g_list_model_get_item (G_LIST_MODEL (manager), i++))) {
+    g_autoptr (EphySearchEngine) engine = EPHY_SEARCH_ENGINE (item);
+    GVariantDict dict;
+
+    g_assert (EPHY_IS_SEARCH_ENGINE (engine));
+
+    g_variant_dict_init (&dict, NULL);
+    g_variant_dict_insert (&dict, "name", "s", ephy_search_engine_get_name (engine));
+    g_variant_dict_insert (&dict, "url", "s", ephy_search_engine_get_url (engine));
+    g_variant_dict_insert (&dict, "bang", "s", ephy_search_engine_get_bang (engine));
+
+    g_variant_builder_add_value (&builder, g_variant_dict_end (&dict));
   }
+  variant = g_variant_builder_end (&builder);
+  g_settings_set_value (EPHY_SETTINGS_MAIN, EPHY_PREFS_SEARCH_ENGINES, variant);
 
-  return search_address;
+  g_settings_set_value (EPHY_SETTINGS_MAIN, EPHY_PREFS_DEFAULT_SEARCH_ENGINE,
+                        g_variant_new_string (ephy_search_engine_get_name (manager->default_engine)));
 }
diff --git a/lib/ephy-search-engine-manager.h b/lib/ephy-search-engine-manager.h
index 9222c7675..c1939d31f 100644
--- a/lib/ephy-search-engine-manager.h
+++ b/lib/ephy-search-engine-manager.h
@@ -24,6 +24,8 @@
 #include <glib-object.h>
 #include <glib/gi18n.h>
 
+#include "ephy-search-engine.h"
+
 G_BEGIN_DECLS
 
 /* TRANSLATORS: Please modify the main address of duckduckgo in order to match
@@ -36,39 +38,20 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (EphySearchEngineManager, ephy_search_engine_manager, EPHY, SEARCH_ENGINE_MANAGER, 
GObject)
 
-EphySearchEngineManager     *ephy_search_engine_manager_new                      (void);
-const char                  *ephy_search_engine_manager_get_address              (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name);
-const char                  *ephy_search_engine_manager_get_default_search_address
-                                                                                 (EphySearchEngineManager 
*manager);
-const char                  *ephy_search_engine_manager_get_bang                 (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name);
-char                        *ephy_search_engine_manager_get_default_engine       (EphySearchEngineManager 
*manager);
-gboolean                     ephy_search_engine_manager_set_default_engine       (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name);
-char                       **ephy_search_engine_manager_get_names                (EphySearchEngineManager 
*manager);
-gboolean                     ephy_search_engine_manager_engine_exists            (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name);
-char                       **ephy_search_engine_manager_get_bangs                (EphySearchEngineManager 
*manager);
-void                         ephy_search_engine_manager_add_engine               (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name,
-                                                                                  const char              
*address,
-                                                                                  const char              
*bang);
-void                         ephy_search_engine_manager_delete_engine            (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name);
-gboolean                     ephy_search_engine_manager_rename                   (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*old_name,
-                                                                                  const char              
*new_name);
-void                         ephy_search_engine_manager_modify_engine            (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name,
-                                                                                  const char              
*address,
-                                                                                  const char              
*bang);
-const char                  *ephy_search_engine_manager_engine_from_bang         (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*bang);
-char                        *ephy_search_engine_manager_build_search_address     (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*name,
-                                                                                  const char              
*search);
-char                        *ephy_search_engine_manager_parse_bang_search        (EphySearchEngineManager 
*manager,
-                                                                                  const char              
*search);
+EphySearchEngineManager *ephy_search_engine_manager_new                 (void);
+EphySearchEngine        *ephy_search_engine_manager_get_default_engine  (EphySearchEngineManager *manager);
+void                     ephy_search_engine_manager_set_default_engine  (EphySearchEngineManager *manager,
+                                                                         EphySearchEngine        *engine);
+void                     ephy_search_engine_manager_add_engine          (EphySearchEngineManager *manager,
+                                                                         EphySearchEngine        *engine);
+void                     ephy_search_engine_manager_delete_engine       (EphySearchEngineManager *manager,
+                                                                         EphySearchEngine        *engine);
+EphySearchEngine        *ephy_search_engine_manager_find_engine_by_name (EphySearchEngineManager *manager,
+                                                                         const char              
*engine_name);
+gboolean                 ephy_search_engine_manager_has_bang            (EphySearchEngineManager *manager,
+                                                                         const char              *bang);
+char                    *ephy_search_engine_manager_parse_bang_search   (EphySearchEngineManager *manager,
+                                                                         const char              *search);
+void                     ephy_search_engine_manager_save_to_settings    (EphySearchEngineManager *manager);
 
 G_END_DECLS
diff --git a/lib/ephy-search-engine.c b/lib/ephy-search-engine.c
new file mode 100644
index 000000000..9e73764d9
--- /dev/null
+++ b/lib/ephy-search-engine.c
@@ -0,0 +1,244 @@
+/* ephy-search-engine.c
+ *
+ * Copyright 2021 vanadiae <vanadiae35 gmail com>
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+#include "ephy-search-engine.h"
+
+#include <libsoup/soup.h>
+
+struct _EphySearchEngine {
+  GObject parent_instance;
+
+  char *name;
+  char *url;
+  char *bang;
+};
+
+G_DEFINE_FINAL_TYPE (EphySearchEngine, ephy_search_engine, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_URL,
+  PROP_BANG,
+  N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+const char *
+ephy_search_engine_get_name (EphySearchEngine *self)
+{
+  return self->name;
+}
+
+void
+ephy_search_engine_set_name (EphySearchEngine *self,
+                             const char       *name)
+{
+  g_assert (name);
+
+  if (g_strcmp0 (name, self->name) == 0)
+    return;
+
+  g_free (self->name);
+  self->name = g_strdup (name);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+}
+
+const char *
+ephy_search_engine_get_url (EphySearchEngine *self)
+{
+  return self->url;
+}
+
+void
+ephy_search_engine_set_url (EphySearchEngine *self,
+                            const char       *url)
+{
+  g_assert (url);
+
+  if (g_strcmp0 (url, self->url) == 0)
+    return;
+
+  g_free (self->url);
+  self->url = g_strdup (url);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_URL]);
+}
+
+const char *
+ephy_search_engine_get_bang (EphySearchEngine *self)
+{
+  return self->bang;
+}
+
+void
+ephy_search_engine_set_bang (EphySearchEngine *self,
+                             const char       *bang)
+{
+  g_assert (bang);
+
+  if (g_strcmp0 (bang, self->bang) == 0)
+    return;
+
+  g_free (self->bang);
+  self->bang = g_strdup (bang);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BANG]);
+}
+
+static void
+ephy_search_engine_finalize (GObject *object)
+{
+  EphySearchEngine *self = (EphySearchEngine *)object;
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->url, g_free);
+  g_clear_pointer (&self->bang, g_free);
+
+  G_OBJECT_CLASS (ephy_search_engine_parent_class)->finalize (object);
+}
+
+static void
+ephy_search_engine_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  EphySearchEngine *self = EPHY_SEARCH_ENGINE (object);
+
+  switch (prop_id) {
+    case PROP_NAME:
+      g_value_set_string (value, ephy_search_engine_get_name (self));
+      break;
+    case PROP_URL:
+      g_value_set_string (value, ephy_search_engine_get_url (self));
+      break;
+    case PROP_BANG:
+      g_value_set_string (value, ephy_search_engine_get_bang (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_search_engine_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  EphySearchEngine *self = EPHY_SEARCH_ENGINE (object);
+
+  switch (prop_id) {
+    case PROP_NAME:
+      ephy_search_engine_set_name (self, g_value_get_string (value));
+      break;
+    case PROP_URL:
+      ephy_search_engine_set_url (self, g_value_get_string (value));
+      break;
+    case PROP_BANG:
+      ephy_search_engine_set_bang (self, g_value_get_string (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+ephy_search_engine_class_init (EphySearchEngineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ephy_search_engine_finalize;
+  object_class->get_property = ephy_search_engine_get_property;
+  object_class->set_property = ephy_search_engine_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "Name",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS |
+                          G_PARAM_EXPLICIT_NOTIFY));
+  properties [PROP_URL] =
+    g_param_spec_string ("url",
+                         "Url",
+                         "The search URL with %s placeholder for this search engine.",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS |
+                          G_PARAM_EXPLICIT_NOTIFY));
+  properties [PROP_BANG] =
+    g_param_spec_string ("bang",
+                         "Bang",
+                         "The search shortcut (bang) for this search engine.",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS |
+                          G_PARAM_EXPLICIT_NOTIFY));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ephy_search_engine_init (EphySearchEngine *self)
+{
+  /* Default values set with the GParamSpec aren't actually set at the end
+   * of the GObject construction process, so we must ensure all properties
+   * we expect to be non NULL to be kept that way, as we want to allow
+   * safely omitting properties when using g_object_new().
+   */
+  self->name = g_strdup ("");
+  self->url = g_strdup ("");
+  self->bang = g_strdup ("");
+}
+
+static char *
+replace_placeholder (const char *url,
+                     const char *search_query)
+{
+  GString *s = g_string_new (url);
+  g_autofree char *encoded_query = soup_form_encode ("q", search_query, NULL);
+
+  /* libsoup requires us to pass a field name to get the HTML-form encoded
+   * search query. But since we don't require that the search URL has the
+   * q= before the placeholder, just skip q= and use the encoded query
+   * directly.
+   */
+  g_string_replace (s, "%s", encoded_query + strlen ("q="), 0);
+
+  return g_string_free (s, FALSE);
+}
+
+/**
+ * ephy_search_engine_build_search_address:
+ * @self: an #EphySearchEngine
+ * @search_query: The search query to be used in the search URL.
+ *
+ * Returns: (transfer full): @self's search URL with all the %s placeholders
+ * replaced with @search_query.
+ */
+char *
+ephy_search_engine_build_search_address (EphySearchEngine *self,
+                                         const char       *search_query)
+{
+  return replace_placeholder (self->url, search_query);
+}
diff --git a/lib/ephy-search-engine.h b/lib/ephy-search-engine.h
new file mode 100644
index 000000000..877def8a2
--- /dev/null
+++ b/lib/ephy-search-engine.h
@@ -0,0 +1,51 @@
+/* ephy-search-engine.h
+ *
+ * Copyright 2021 vanadiae <vanadiae35 gmail com>
+ *
+ * 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define EPHY_TYPE_SEARCH_ENGINE (ephy_search_engine_get_type())
+
+G_DECLARE_FINAL_TYPE (EphySearchEngine, ephy_search_engine, EPHY, SEARCH_ENGINE, GObject)
+
+/* It's intended that there's no ephy_search_engine_new() as that just can't be
+ * general enough to cover all the cases where you'd create an engine. So instead,
+ * just use g_object_new() with the properties you already have available for the
+ * new search engine, and all other properties will
+ * have a reasonable default value (i.e. empty or NULL).
+ */
+
+const char *ephy_search_engine_get_name             (EphySearchEngine *self);
+void        ephy_search_engine_set_name             (EphySearchEngine *self,
+                                                     const char       *name);
+const char *ephy_search_engine_get_url              (EphySearchEngine *self);
+void        ephy_search_engine_set_url              (EphySearchEngine *self,
+                                                     const char       *url);
+const char *ephy_search_engine_get_bang             (EphySearchEngine *self);
+void        ephy_search_engine_set_bang             (EphySearchEngine *self,
+                                                     const char       *bang);
+char       *ephy_search_engine_build_search_address (EphySearchEngine *self,
+                                                     const char       *search_query);
+
+G_END_DECLS
diff --git a/lib/meson.build b/lib/meson.build
index 264f9c5fb..a8d774757 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -23,6 +23,7 @@ libephymisc_sources = [
   'ephy-output-encoding.c',
   'ephy-permissions-manager.c',
   'ephy-profile-utils.c',
+  'ephy-search-engine.c',
   'ephy-search-engine-manager.c',
   'ephy-security-levels.c',
   'ephy-settings.c',
diff --git a/src/ephy-suggestion-model.c b/src/ephy-suggestion-model.c
index 101dcb7d4..3299e2db6 100644
--- a/src/ephy-suggestion-model.c
+++ b/src/ephy-suggestion-model.c
@@ -320,24 +320,25 @@ add_search_engines (EphySuggestionModel *self,
 {
   EphyEmbedShell *shell;
   EphySearchEngineManager *manager;
-  char **engines;
-  guint added = 0;
+  guint added;
 
   shell = ephy_embed_shell_get_default ();
   manager = ephy_embed_shell_get_search_engine_manager (shell);
-  engines = ephy_search_engine_manager_get_names (manager);
 
-  for (guint i = 0; engines[i] != NULL; i++) {
+  for (added = 0; added < g_list_model_get_n_items (G_LIST_MODEL (manager)); added++) {
+    g_autoptr (EphySearchEngine) engine = g_list_model_get_item (G_LIST_MODEL (manager), added);
     EphySuggestion *suggestion;
+    const char *engine_name;
     g_autofree char *address = NULL;
     g_autofree char *escaped_title = NULL;
     g_autofree char *markup = NULL;
     g_autoptr (GUri) uri = NULL;
 
-    address = ephy_search_engine_manager_build_search_address (manager, engines[i], query);
-    escaped_title = g_markup_escape_text (engines[i], -1);
+    engine_name = ephy_search_engine_get_name (engine);
+    address = ephy_search_engine_build_search_address (engine, query);
+    escaped_title = g_markup_escape_text (engine_name, -1);
     markup = dzl_fuzzy_highlight (escaped_title, query, FALSE);
-    suggestion = ephy_suggestion_new (markup, engines[i], address);
+    suggestion = ephy_suggestion_new (markup, engine_name, address);
 
     uri = g_uri_parse (address, G_URI_FLAGS_NONE, NULL);
     if (uri) {
@@ -348,13 +349,11 @@ add_search_engines (EphySuggestionModel *self,
     load_favicon (self, suggestion, address);
 
     g_sequence_append (self->items, suggestion);
-    added++;
   }
 
-  g_strfreev (engines);
-
   return added;
 }
+
 typedef struct {
   char *query;
   char scope;
@@ -656,7 +655,7 @@ google_search_suggestions_cb (SoupSession *session,
   JsonNode *node;
   JsonArray *array;
   JsonArray *suggestions;
-  char *engine;
+  EphySearchEngine *engine;
   int added = 0;
   g_autoptr (GBytes) body = NULL;
 #if SOUP_CHECK_VERSION (2, 99, 4)
@@ -698,11 +697,13 @@ google_search_suggestions_cb (SoupSession *session,
     g_autofree char *address = NULL;
     g_autofree char *escaped_title = NULL;
     g_autofree char *markup = NULL;
+    const char *engine_name;
 
-    address = ephy_search_engine_manager_build_search_address (manager, engine, str);
+    address = ephy_search_engine_build_search_address (engine, str);
     escaped_title = g_markup_escape_text (str, -1);
     markup = dzl_fuzzy_highlight (escaped_title, str, FALSE);
-    suggestion = ephy_suggestion_new (markup, engine, address);
+    engine_name = ephy_search_engine_get_name (engine);
+    suggestion = ephy_suggestion_new (markup, engine_name, address);
 
     g_sequence_append (data->google_suggestions, g_steal_pointer (&suggestion));
     added++;
diff --git a/src/ephy-window.c b/src/ephy-window.c
index ab8caedf5..0bc8be18d 100644
--- a/src/ephy-window.c
+++ b/src/ephy-window.c
@@ -4031,16 +4031,15 @@ ephy_window_location_search (EphyWindow *window)
   GtkApplication *gtk_application = gtk_window_get_application (GTK_WINDOW (window));
   EphyEmbedShell *embed_shell = EPHY_EMBED_SHELL (gtk_application);
   EphySearchEngineManager *search_engine_manager = ephy_embed_shell_get_search_engine_manager (embed_shell);
-  char *search_engine_name = ephy_search_engine_manager_get_default_engine (search_engine_manager);
-  const char *search_engine_bang = ephy_search_engine_manager_get_bang (search_engine_manager, 
search_engine_name);
-  char *entry_text = g_strconcat (search_engine_bang, " ", NULL);
+  EphySearchEngine *default_engine = ephy_search_engine_manager_get_default_engine (search_engine_manager);
+  const char *bang = ephy_search_engine_get_bang (default_engine);
+  char *entry_text = g_strconcat (bang, " ", NULL);
 
   gtk_window_set_focus (GTK_WINDOW (window), GTK_WIDGET (location_gtk_entry));
   gtk_entry_set_text (location_gtk_entry, entry_text);
   gtk_editable_set_position (GTK_EDITABLE (location_gtk_entry), strlen (entry_text));
 
   g_free (entry_text);
-  g_free (search_engine_name);
 }
 
 /**
diff --git a/src/preferences/ephy-prefs-dialog.c b/src/preferences/ephy-prefs-dialog.c
index 6089b0860..29addb886 100644
--- a/src/preferences/ephy-prefs-dialog.c
+++ b/src/preferences/ephy-prefs-dialog.c
@@ -24,10 +24,11 @@
 
 #include "clear-data-view.h"
 #include "ephy-data-view.h"
+#include "ephy-embed-shell.h"
 #include "ephy-embed-utils.h"
 #include "ephy-gui.h"
 #include "ephy-prefs-dialog.h"
-#include "ephy-settings.h"
+#include "ephy-search-engine-manager.h"
 #include "passwords-view.h"
 #include "prefs-general-page.h"
 
@@ -61,7 +62,11 @@ on_delete_event (EphyPrefsDialog *prefs_dialog)
 {
   prefs_general_page_on_pd_delete_event (prefs_dialog->general_page);
   gtk_widget_destroy (GTK_WIDGET (prefs_dialog));
-  g_settings_apply (EPHY_SETTINGS_MAIN);
+
+  /* To avoid any unnecessary IO when typing changes in the search engine
+   * list row's entries, only save when closing the prefs dialog.
+   */
+  ephy_search_engine_manager_save_to_settings (ephy_embed_shell_get_search_engine_manager 
(ephy_embed_shell_get_default ()));
 }
 
 static void
@@ -133,6 +138,4 @@ ephy_prefs_dialog_init (EphyPrefsDialog *dialog)
   gtk_window_set_icon_name (GTK_WINDOW (dialog), APPLICATION_ID);
 
   ephy_gui_ensure_window_group (GTK_WINDOW (dialog));
-
-  g_settings_delay (EPHY_SETTINGS_MAIN);
 }
diff --git a/src/preferences/ephy-search-engine-listbox.c b/src/preferences/ephy-search-engine-listbox.c
index bdf407b3f..0f0788b5f 100644
--- a/src/preferences/ephy-search-engine-listbox.c
+++ b/src/preferences/ephy-search-engine-listbox.c
@@ -26,14 +26,165 @@
 #include "embed/ephy-embed-shell.h"
 #include "ephy-search-engine-manager.h"
 
+#define EMPTY_NEW_SEARCH_ENGINE_NAME (_("New search engine"))
+
+
+/* Goes with _EphyAddEngineButtonMergedModel. Used as a way of detecting if this
+ * is the list model item where we should be creating the "Add search engine" row
+ * instead of the normal EphySearchEngineRow.
+ */
+#define EPHY_TYPE_ADD_SEARCH_ENGINE_ROW_ITEM (ephy_add_search_engine_row_item_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyAddSearchEngineRowItem, ephy_add_search_engine_row_item, EPHY, 
ADD_SEARCH_ENGINE_ROW_ITEM, GObject)
+
+struct _EphyAddSearchEngineRowItem {
+  GObject parent_instance;
+};
+
+G_DEFINE_TYPE (EphyAddSearchEngineRowItem, ephy_add_search_engine_row_item, G_TYPE_OBJECT)
+
+static void ephy_add_search_engine_row_item_class_init (EphyAddSearchEngineRowItemClass *klass) {}
+
+static void ephy_add_search_engine_row_item_init (EphyAddSearchEngineRowItem *self) {}
+
+/* This model is only needed because we want to use gtk_list_box_bind_model()
+ * while having a "Add search engine" row. In GTK4 we could get our way out
+ * using GtkFlattenListModel and 1-item GListStore for the outer and inner (i.e.
+ * the list model that only contains one item to indicate "this is an "Add search engine" row")
+ * required list models. But we're not GTK4 yet, so we need to proxy all list model
+ * calls appropriately to the EphySearchEngineManager one ourselves.
+ */
+#define EPHY_TYPE_ADD_ENGINE_BUTTON_MERGED_MODEL (ephy_add_engine_button_merged_model_get_type ())
+
+G_DECLARE_FINAL_TYPE (EphyAddEngineButtonMergedModel, ephy_add_engine_button_merged_model, EPHY, 
ADD_ENGINE_BUTTON_MERGED_MODEL, GObject)
+
+struct _EphyAddEngineButtonMergedModel {
+  GObject parent_instance;
+
+  GListModel *model;
+  EphyAddSearchEngineRowItem *add_engine_row_item;
+};
+
+static void list_model_iface_init (GListModelInterface *iface,
+                                   gpointer             iface_data);
+
+G_DEFINE_TYPE_WITH_CODE (EphyAddEngineButtonMergedModel, ephy_add_engine_button_merged_model, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+inner_model_items_changed_cb (GListModel *list,
+                              guint       position,
+                              guint       removed,
+                              guint       added,
+                              gpointer    user_data)
+{
+  EphyAddEngineButtonMergedModel *self = user_data;
+
+  /* Since we place our custom item at the end of the list model, we can pass
+   * items-changed informations unchanged.
+   */
+  g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added);
+}
+
+static GType
+list_model_get_item_type_func (GListModel *list)
+{
+  /* Yes, we're doing this so that EPHY_IS_SEARCH_ENGINE() and
+   * EPHY_IS_ADD_SEARCH_ENGINE_ROW_ITEM() will work later.
+   */
+  return G_TYPE_OBJECT;
+}
+
+static guint
+list_model_get_n_items_func (GListModel *list)
+{
+  EphyAddEngineButtonMergedModel *self = (gpointer)list;
+
+  /* +1 for the "add search engine" row placeholder object. */
+  return g_list_model_get_n_items (self->model) + 1;
+}
+
+static gpointer
+list_model_get_item_func (GListModel *list,
+                          guint       position)
+{
+  EphyAddEngineButtonMergedModel *self = (gpointer)list;
+
+  gpointer item = g_list_model_get_item (self->model, position);
+
+  if (item)
+    return item;
+  else {
+    guint n_items = g_list_model_get_n_items (self->model);
+
+    /* Normally, n_items is the length of the list model, so it can't have
+     * an associated item in the list model. So when we reach that case,
+     * return our "add search engine" row item. Else, we're out of the
+     * n_items + 1 range and so we must return NULL to indicate the end
+     * of the list model.
+     */
+    if (position == n_items)
+      return g_object_ref (self->add_engine_row_item);
+    else
+      return NULL;
+  }
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface,
+                       gpointer             iface_data)
+{
+  iface->get_item_type = list_model_get_item_type_func;
+  iface->get_n_items = list_model_get_n_items_func;
+  iface->get_item = list_model_get_item_func;
+}
+
+static void
+ephy_add_engine_button_merged_model_finalize (GObject *object)
+{
+  EphyAddEngineButtonMergedModel *self = (gpointer)object;
+
+  g_clear_object (&self->add_engine_row_item);
+  self->model = NULL;
+
+  G_OBJECT_CLASS (ephy_add_engine_button_merged_model_parent_class)->finalize (object);
+}
+
+static void
+ephy_add_engine_button_merged_model_class_init (EphyAddEngineButtonMergedModelClass *klass)
+{
+  GObjectClass *o_class = G_OBJECT_CLASS (klass);
+
+  o_class->finalize = ephy_add_engine_button_merged_model_finalize;
+}
+
+static void
+ephy_add_engine_button_merged_model_init (EphyAddEngineButtonMergedModel *self)
+{
+  self->model = G_LIST_MODEL (ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ()));
+  self->add_engine_row_item = g_object_new (EPHY_TYPE_ADD_SEARCH_ENGINE_ROW_ITEM, NULL);
+
+  g_signal_connect_object (self->model, "items-changed", G_CALLBACK (inner_model_items_changed_cb), self, 0);
+}
+
 struct _EphySearchEngineListBox {
   GtkListBox parent_instance;
 
   /* This widget isn't actually showed anywhere. It is just a stable place where we can add more radio 
buttons without having to bother if the primary radio button gets removed. */
   GtkWidget *radio_buttons_group;
-
   GtkWidget *add_search_engine_row;
+
+  EphySearchEngine *empty_new_search_engine;
   EphySearchEngineManager *manager;
+
+  EphyAddEngineButtonMergedModel *wrapper_model;
+
+  /* Used as a flag to avoid expanding the newly created row (which is our
+   * default behaviour). It'll only be set to TRUE after the model has been
+   * bound to the list box. This avoids having to iterate the list box to
+   * unexpand all rows after having expanded them all.
+   */
+  gboolean is_model_initially_loaded;
 };
 
 G_DEFINE_TYPE (EphySearchEngineListBox, ephy_search_engine_list_box, GTK_TYPE_LIST_BOX)
@@ -44,23 +195,61 @@ ephy_search_engine_list_box_new (void)
   return g_object_new (EPHY_TYPE_SEARCH_ENGINE_LIST_BOX, NULL);
 }
 
-/**
- * ephy_search_engine_listbox_set_can_add_engine:
- *
- * Sets whether the "Add search engine" row of @self is sensitive.
- *
- * @self: a #EphySearchEngineListBox
- * @can_add_engine: whether the user can add new search engines to @self
- */
-void
-ephy_search_engine_list_box_set_can_add_engine (EphySearchEngineListBox *self,
-                                                gboolean                 can_add_engine)
+static void
+on_search_engine_name_changed_cb (EphySearchEngine        *engine,
+                                  GParamSpec              *pspec,
+                                  EphySearchEngineListBox *self)
 {
-  gtk_widget_set_sensitive (self->add_search_engine_row, can_add_engine);
+  const char *name = ephy_search_engine_get_name (engine);
+
+  /* If that's the empty search engine, then we keep it internally marked
+   * as "this was the empty search engine", since we don't have a way
+   * of knowing the previous name in notify:: callbacks. That won't be an
+   * issue even if someone tries naming another search engine with the same
+   * EMPTY_NEW_SEARCH_ENGINE_NAME, as the row entry's validation prevents
+   * this from happening (it checks if there's already a search engine with
+   * that name before setting this particular engine's name in the manager).
+   */
+  if (g_strcmp0 (name, EMPTY_NEW_SEARCH_ENGINE_NAME) == 0) {
+    self->empty_new_search_engine = engine;
+    gtk_widget_set_sensitive (self->add_search_engine_row, FALSE);
+  }
+  /* This search engine was the only "new empty" one, and it is no longer
+   * the "new empty" search engine, so allow adding a new search engine again.
+   */
+  else if (engine == self->empty_new_search_engine &&
+           g_strcmp0 (name, EMPTY_NEW_SEARCH_ENGINE_NAME) != 0) {
+    self->empty_new_search_engine = NULL;
+    gtk_widget_set_sensitive (self->add_search_engine_row, TRUE);
+  }
 }
 
-/***** Private *****/
+static void
+on_list_box_manager_items_changed_cb (GListModel *list,
+                                      guint       position,
+                                      guint       removed,
+                                      guint       added,
+                                      gpointer    user_data)
+{
+  EphySearchEngineListBox *self = EPHY_SEARCH_ENGINE_LIST_BOX (user_data);
+  EphySearchEngineManager *manager = EPHY_SEARCH_ENGINE_MANAGER (list);
+
+  /* This callback is mostly only called when a search engine has been removed
+   * (potentially the new empty one), when clicking the Add Search Engine button
+   * (in which case we'll want to make the button insensitive), or when initially
+   * loading all the search engines. In all those cases, we check if we have the
+   * "empty new search engine" and update the Add Search Engine's button sensitivity
+   * based on it.
+   */
+  self->empty_new_search_engine = ephy_search_engine_manager_find_engine_by_name (manager, 
EMPTY_NEW_SEARCH_ENGINE_NAME);
 
+  gtk_widget_set_sensitive (self->add_search_engine_row,
+                            self->empty_new_search_engine == NULL);
+}
+
+/* This signal unexpands all other rows of the list box except the row
+ * that just got expanded.
+ */
 static void
 on_row_expand_state_changed_cb (EphySearchEngineRow     *expanded_row,
                                 GParamSpec              *pspec,
@@ -87,82 +276,99 @@ on_row_expand_state_changed_cb (EphySearchEngineRow     *expanded_row,
   }
 }
 
-/**
- * append_search_engine_row:
- *
- * Creates a new row showing search engine @engine_name, and adds
- * it to @search_engine_list_box.
- *
- * @search_engine_list_box: an #EphySearchEngineListBox
- * @engine_name: the name of an already existing engine in @search_engine_list_box->manager which will be 
presented as a new row
- *
- * Returns: the newly added row.
- */
-static EphySearchEngineRow *
-append_search_engine_row (EphySearchEngineListBox *list_box,
-                          const char              *engine_name)
+static void
+on_add_search_engine_row_clicked_cb (EphySearchEngineListBox *self,
+                                     GtkListBoxRow           *clicked_row,
+                                     gpointer                 user_data)
 {
-  EphySearchEngineRow *new_row = ephy_search_engine_row_new (engine_name);
+  g_autoptr (EphySearchEngine) empty_engine = NULL;
 
-  gtk_list_box_prepend (GTK_LIST_BOX (list_box),
-                        GTK_WIDGET (new_row));
-  ephy_search_engine_row_set_radio_button_group (new_row,
-                                                 GTK_RADIO_BUTTON (list_box->radio_buttons_group));
-  g_signal_connect (new_row,
-                    "notify::expanded",
-                    G_CALLBACK (on_row_expand_state_changed_cb),
-                    list_box);
+  /* Sanity check. Expander rows aren't supposed to be activable=True. */
+  g_assert ((gpointer)clicked_row == (gpointer)self->add_search_engine_row);
 
-  return new_row;
+  empty_engine = g_object_new (EPHY_TYPE_SEARCH_ENGINE,
+                               "name", EMPTY_NEW_SEARCH_ENGINE_NAME,
+                               "url", "https://www.example.com/search?q=%s";,
+                               NULL);
+  ephy_search_engine_manager_add_engine (self->manager, empty_engine);
+
+  /* In on_search_engine_name_changed_cb above, we set the Add search engine
+   * row's sensitivity based on whether there is an engine named EMPTY_NEW_SEARCH_ENGINE_NAME.
+   */
 }
 
-static void
-on_add_search_engine_row_clicked_cb (EphySearchEngineListBox *search_engine_list_box,
-                                     GtkListBoxRow           *add_search_engine_row,
-                                     gpointer                 user_data)
+static GtkWidget *
+create_add_search_engine_row ()
 {
-  GtkWidget *search_engine_row;
+  GtkWidget *row = gtk_list_box_row_new ();
+  GtkWidget *label = gtk_label_new_with_mnemonic (_("A_dd Search Engine…"));
 
-  g_assert (add_search_engine_row == GTK_LIST_BOX_ROW (search_engine_list_box->add_search_engine_row));
+  gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), true);
+  gtk_widget_set_size_request (row, -1, 50);
+  gtk_widget_show (row);
 
-  /* Allow to remove the row if it was alone */
-  if (gtk_list_box_get_row_at_index (GTK_LIST_BOX (search_engine_list_box), 2) == NULL)
-    ephy_search_engine_row_set_can_remove (EPHY_SEARCH_ENGINE_ROW (gtk_list_box_get_row_at_index 
(GTK_LIST_BOX (search_engine_list_box), 0)),
-                                           TRUE);
-  ephy_search_engine_manager_add_engine (search_engine_list_box->manager,
-                                         EMPTY_NEW_SEARCH_ENGINE_NAME,
-                                         "",
-                                         "");
-  search_engine_row = GTK_WIDGET (append_search_engine_row (search_engine_list_box, 
EMPTY_NEW_SEARCH_ENGINE_NAME));
-  hdy_expander_row_set_expanded (HDY_EXPANDER_ROW (search_engine_row), TRUE);
-  /* Only allow one empty search engine to be created. This row will be sensitive again when the empty row 
is renamed. */
-  gtk_widget_set_sensitive (GTK_WIDGET (add_search_engine_row), FALSE);
+  gtk_widget_show (label);
+  gtk_container_add (GTK_CONTAINER (row), label);
+
+  return row;
 }
 
-static void
-ephy_search_engine_list_box_finalize (GObject *object)
+static GtkWidget *
+create_search_engine_row (EphySearchEngine        *engine,
+                          EphySearchEngineListBox *self)
 {
-  EphySearchEngineListBox *self = (EphySearchEngineListBox *)object;
+  EphySearchEngineRow *row = ephy_search_engine_row_new (engine, self->manager);
 
-  g_clear_pointer (&self->radio_buttons_group, g_object_unref);
+  g_signal_connect_object (engine, "notify::name", G_CALLBACK (on_search_engine_name_changed_cb), self, 0);
 
-  G_OBJECT_CLASS (ephy_search_engine_list_box_parent_class)->finalize (object);
+  ephy_search_engine_row_set_radio_button_group (row,
+                                                 GTK_RADIO_BUTTON (self->radio_buttons_group));
+  g_signal_connect (row,
+                    "notify::expanded",
+                    G_CALLBACK (on_row_expand_state_changed_cb),
+                    self);
+
+  /* This check ensures we don't try expanding all rows when we initially bind
+   * the model to the list box.
+   */
+  if (self->is_model_initially_loaded) {
+    /* This will also unexpand all other rows, to make the new one stand out,
+     * in on_row_expand_state_changed_cb().
+     */
+    hdy_expander_row_set_expanded (HDY_EXPANDER_ROW (row), TRUE);
+  }
+
+  return GTK_WIDGET (row);
+}
+
+static GtkWidget *
+list_box_create_row_func (gpointer item,
+                          gpointer user_data)
+{
+  EphySearchEngineListBox *self = EPHY_SEARCH_ENGINE_LIST_BOX (user_data);
+
+  g_assert (item != NULL);
+
+  if (EPHY_IS_SEARCH_ENGINE (item)) {
+    EphySearchEngine *engine = item;
+    return create_search_engine_row (engine, self);
+  } else if (EPHY_IS_ADD_SEARCH_ENGINE_ROW_ITEM (item)) {
+    self->add_search_engine_row = create_add_search_engine_row ();
+    return self->add_search_engine_row;
+  } else {
+    g_assert_not_reached ();
+  }
 }
 
 static void
-populate_search_engine_list_box (EphySearchEngineListBox *self)
+ephy_search_engine_list_box_finalize (GObject *object)
 {
-  g_auto (GStrv) engine_names = ephy_search_engine_manager_get_names (self->manager);
-  g_autofree char *default_engine = ephy_search_engine_manager_get_default_engine (self->manager);
+  EphySearchEngineListBox *self = (EphySearchEngineListBox *)object;
 
-  for (guint i = 0; engine_names[i] != NULL; ++i) {
-    EphySearchEngineRow *row = append_search_engine_row (self, engine_names[i]);
-    if (g_strcmp0 (engine_names[i], default_engine) == 0)
-      ephy_search_engine_row_set_as_default (row);
-  }
+  g_clear_object (&self->radio_buttons_group);
+  g_clear_object (&self->wrapper_model);
 
-  if (ephy_search_engine_manager_engine_exists (self->manager, EMPTY_NEW_SEARCH_ENGINE_NAME))
-    gtk_widget_set_sensitive (self->add_search_engine_row, FALSE);
+  G_OBJECT_CLASS (ephy_search_engine_list_box_parent_class)->finalize (object);
 }
 
 static void
@@ -175,7 +381,6 @@ ephy_search_engine_list_box_class_init (EphySearchEngineListBoxClass *klass)
 
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/epiphany/gtk/search-engine-listbox.ui");
 
-  gtk_widget_class_bind_template_child (widget_class, EphySearchEngineListBox, add_search_engine_row);
   gtk_widget_class_bind_template_callback (widget_class, on_add_search_engine_row_clicked_cb);
 }
 
@@ -193,12 +398,30 @@ ephy_search_engine_list_box_init (EphySearchEngineListBox *self)
    */
   g_object_ref_sink (self->radio_buttons_group);
 
-  gtk_list_box_set_sort_func (GTK_LIST_BOX (self), ephy_search_engine_row_get_sort_func (), NULL, NULL);
-  gtk_list_box_invalidate_sort (GTK_LIST_BOX (self));
-
-  populate_search_engine_list_box (self);
-
-  /* The list box should have at least one "Add search engine" row and one search engine (the default one). 
*/
-  if (gtk_list_box_get_row_at_index (GTK_LIST_BOX (self), 2) == NULL)
-    ephy_search_engine_row_set_can_remove (EPHY_SEARCH_ENGINE_ROW (gtk_list_box_get_row_at_index 
(GTK_LIST_BOX (self), 0)), FALSE);
+  self->wrapper_model = g_object_new (EPHY_TYPE_ADD_ENGINE_BUTTON_MERGED_MODEL, NULL);
+  self->is_model_initially_loaded = FALSE;
+  gtk_list_box_bind_model (GTK_LIST_BOX (self),
+                           G_LIST_MODEL (self->wrapper_model),
+                           (GtkListBoxCreateWidgetFunc)list_box_create_row_func,
+                           self, NULL);
+  self->is_model_initially_loaded = TRUE;
+
+  /* When the row's radio button gets parented all the way up to the window,
+   * it seems like GTK sets one of the radio button in the group as clicked,
+   * but messes things up somewhere. Whatever we do to click or not click this
+   * particular radio button when creating our row widget depending on whether
+   * it is the default engine, all the rows end up not "ticked". To circumvent
+   * this, just trick the manager into sending a dummy notify:: signal so that
+   * the row which matches the default engine updates its own radio button state.
+   * This is the cleanest way I found to workaround the issue.
+   */
+  ephy_search_engine_manager_set_default_engine (self->manager,
+                                                 ephy_search_engine_manager_get_default_engine 
(self->manager));
+
+  on_list_box_manager_items_changed_cb (G_LIST_MODEL (self->manager),
+                                        0,
+                                        0,
+                                        g_list_model_get_n_items (G_LIST_MODEL (self->manager)),
+                                        self);
+  g_signal_connect_object (self->manager, "items-changed", G_CALLBACK 
(on_list_box_manager_items_changed_cb), self, 0);
 }
diff --git a/src/preferences/ephy-search-engine-listbox.h b/src/preferences/ephy-search-engine-listbox.h
index 125d3bb8b..624901f31 100644
--- a/src/preferences/ephy-search-engine-listbox.h
+++ b/src/preferences/ephy-search-engine-listbox.h
@@ -23,8 +23,6 @@
 #include <glib-object.h>
 #include <gtk/gtk.h>
 
-#define EMPTY_NEW_SEARCH_ENGINE_NAME (_("New search engine"))
-
 G_BEGIN_DECLS
 
 #define EPHY_TYPE_SEARCH_ENGINE_LIST_BOX (ephy_search_engine_list_box_get_type())
@@ -33,7 +31,4 @@ G_DECLARE_FINAL_TYPE (EphySearchEngineListBox, ephy_search_engine_list_box, EPHY
 
 GtkWidget *ephy_search_engine_list_box_new                (void);
 
-void       ephy_search_engine_list_box_set_can_add_engine (EphySearchEngineListBox *self,
-                                                           gboolean                 can_add_engine);
-
 G_END_DECLS
diff --git a/src/preferences/ephy-search-engine-row.c b/src/preferences/ephy-search-engine-row.c
index 4b76469d7..257c8486c 100644
--- a/src/preferences/ephy-search-engine-row.c
+++ b/src/preferences/ephy-search-engine-row.c
@@ -39,13 +39,7 @@ struct _EphySearchEngineRow {
   GtkWidget *remove_button;
   GtkWidget *radio_button;
 
-  /* This is only used to be able to rename the old search engine with a new name,
-   * and to access the search engine's informations stored in the @manager.
-   * It is always a valid name.
-   */
-  char *saved_name;
-  /* This is the name that was previously in the entry. Use this only from on_name_entry_text_changed_cb() */
-  char *previous_name;
+  EphySearchEngine *engine;
   EphySearchEngineManager *manager;
 };
 
@@ -53,7 +47,8 @@ G_DEFINE_TYPE (EphySearchEngineRow, ephy_search_engine_row, HDY_TYPE_EXPANDER_RO
 
 enum {
   PROP_0,
-  PROP_SEARCH_ENGINE_NAME,
+  PROP_SEARCH_ENGINE,
+  PROP_MANAGER,
   N_PROPS
 };
 
@@ -63,65 +58,25 @@ static GParamSpec *properties[N_PROPS];
 
 /**
  * ephy_search_engine_row_new:
+ * @search_engine: the search engine to show. This search engine must already
+ *   exist in @manager.
+ * @manager: The search engine manager to which @search_engine belongs.
  *
- * Creates a new #EphySearchEngineRow showing @search_engine_name engine informations.
- *
- * @search_engine_name: the name of the search engine to show.
- * This search engine must already exist in the default search engine manager.
+ * Creates a new #EphySearchEngineRow showing @search_engine informations and
+ * allowing to edit them.
  *
  * Returns: a newly created #EphySearchEngineRow
  */
 EphySearchEngineRow *
-ephy_search_engine_row_new (const char *search_engine_name)
+ephy_search_engine_row_new (EphySearchEngine        *engine,
+                            EphySearchEngineManager *manager)
 {
   return g_object_new (EPHY_TYPE_SEARCH_ENGINE_ROW,
-                       "search-engine-name", search_engine_name,
+                       "search-engine", engine,
+                       "manager", manager,
                        NULL);
 }
 
-static int
-sort_search_engine_list_box_cb (EphySearchEngineRow *first_row,
-                                EphySearchEngineRow *second_row,
-                                gpointer             user_data)
-{
-  g_autofree char *first_row_name = NULL;
-  g_autofree char *second_row_name = NULL;
-
-  /* Place the "add search engine" row at the end.
-   * This row isn't an expander row, only a regular row.
-   */
-  if (!EPHY_IS_SEARCH_ENGINE_ROW (first_row))
-    return 1;
-  if (!EPHY_IS_SEARCH_ENGINE_ROW (second_row))
-    return -1;
-
-  first_row_name = g_utf8_casefold (first_row->saved_name, -1);
-  second_row_name = g_utf8_casefold (second_row->saved_name, -1);
-
-  return g_strcmp0 (first_row_name, second_row_name);
-}
-
-GtkListBoxSortFunc
-ephy_search_engine_row_get_sort_func (void)
-{
-  return (GtkListBoxSortFunc)sort_search_engine_list_box_cb;
-}
-
-/**
- * ephy_search_engine_row_set_can_remove:
- *
- * Sets whether the Remove button of @self is sensitive.
- *
- * @self: an #EphySearchEngineRow
- * @can_remove: whether the user can click the @self's Remove button
- */
-void
-ephy_search_engine_row_set_can_remove (EphySearchEngineRow *self,
-                                       gboolean             can_remove)
-{
-  gtk_widget_set_sensitive (self->remove_button, can_remove);
-}
-
 /**
  * ephy_search_engine_row_set_radio_button_group:
  *
@@ -138,47 +93,8 @@ ephy_search_engine_row_set_radio_button_group (EphySearchEngineRow *self,
                               gtk_radio_button_get_group (radio_button_group));
 }
 
-/**
- * ephy_search_engine_row_set_as_default:
- *
- * Sets this search engine represented by @self as the default engine for
- * the default search engine manager. In practice, it toggles the default engine radio button.
- *
- * @self: an #EphySearchEngineRow
- */
-void
-ephy_search_engine_row_set_as_default (EphySearchEngineRow *self)
-{
-  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->radio_button), TRUE);
-}
-
 /***** Private implementation *****/
 
-static gboolean
-search_engine_already_exists (EphySearchEngineRow *searched_row,
-                              const char          *engine_name)
-{
-  GList *children = gtk_container_get_children (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET 
(searched_row))));
-
-  for (; children->next != NULL; children = children->next) {
-    EphySearchEngineRow *iterated_row;
-
-    /* As it iterates on the whole list box, this function will run on the "add search engine" row, which 
isn't an EphySearchEngineRow. */
-    if (!EPHY_IS_SEARCH_ENGINE_ROW (children->data))
-      continue;
-
-    iterated_row = EPHY_SEARCH_ENGINE_ROW (children->data);
-
-    if (iterated_row == searched_row)
-      continue;
-
-    if (g_strcmp0 (iterated_row->saved_name, engine_name) == 0)
-      return TRUE;
-  }
-
-  return FALSE;
-}
-
 /**
  * validate_search_engine_address:
  *
@@ -268,10 +184,10 @@ on_bang_entry_text_changed_cb (EphySearchEngineRow *row,
                                GtkEntry            *bang_entry)
 {
   const char *bang = gtk_entry_get_text (bang_entry);
-  const char *engine_from_bang = ephy_search_engine_manager_engine_from_bang (row->manager, bang);
 
   /* Checks if the bang already exists */
-  if (engine_from_bang && g_strcmp0 (engine_from_bang, row->saved_name) != 0) {
+  if (g_strcmp0 (bang, ephy_search_engine_get_bang (row->engine)) != 0 &&
+      ephy_search_engine_manager_has_bang (row->manager, bang)) {
     set_entry_as_invalid (bang_entry, _("This shortcut is already used."));
   } else if (strchr (bang, ' ') != NULL) {
     set_entry_as_invalid (bang_entry, _("Search shortcuts must not contain any space."));
@@ -285,10 +201,7 @@ on_bang_entry_text_changed_cb (EphySearchEngineRow *row,
     set_entry_as_invalid (bang_entry, _("Search shortcuts should start with a symbol such as !, # or @."));
   } else {
     set_entry_as_valid (bang_entry);
-    ephy_search_engine_manager_modify_engine (row->manager,
-                                              row->saved_name,
-                                              ephy_search_engine_manager_get_address (row->manager, 
row->saved_name),
-                                              gtk_entry_get_text (bang_entry));
+    ephy_search_engine_set_bang (row->engine, bang);
   }
 }
 
@@ -298,17 +211,14 @@ on_address_entry_text_changed_cb (EphySearchEngineRow *row,
                                   GtkEntry            *address_entry)
 {
   const char *validation_message = NULL;
+  const char *url = gtk_entry_get_text (address_entry);
 
   /* Address in invalid. */
-  if (!validate_search_engine_address (gtk_entry_get_text (address_entry), &validation_message)) {
+  if (!validate_search_engine_address (url, &validation_message)) {
     set_entry_as_invalid (address_entry, validation_message);
   } else { /* Address in valid. */
     set_entry_as_valid (address_entry);
-    ephy_search_engine_manager_modify_engine (row->manager,
-                                              row->saved_name,
-                                              gtk_entry_get_text (address_entry),
-                                              ephy_search_engine_manager_get_bang (row->manager,
-                                                                                   row->saved_name));
+    ephy_search_engine_set_url (row->engine, url);
   }
 }
 
@@ -401,10 +311,7 @@ update_bang_for_name (EphySearchEngineRow *row,
   lowercase_acronym = g_utf8_strdown (acronym, -1); /* Bangs are usually lowercase */
   final_bang = g_strconcat ("!", lowercase_acronym, NULL); /* "!" is the prefix for the bang */
   gtk_entry_set_text (GTK_ENTRY (row->bang_entry), final_bang);
-  ephy_search_engine_manager_modify_engine (row->manager,
-                                            row->saved_name,
-                                            ephy_search_engine_manager_get_address (row->manager, 
row->saved_name),
-                                            gtk_entry_get_text (GTK_ENTRY (row->bang_entry)));
+  ephy_search_engine_set_bang (row->engine, final_bang);
 }
 
 static void
@@ -412,7 +319,6 @@ on_name_entry_text_changed_cb (EphySearchEngineRow *row,
                                GParamSpec          *pspec,
                                GtkEntry            *name_entry)
 {
-  EphySearchEngineListBox *search_engine_list_box = EPHY_SEARCH_ENGINE_LIST_BOX (gtk_widget_get_parent 
(GTK_WIDGET (row)));
   const char *new_name = gtk_entry_get_text (name_entry);
 
   /* This is an edge case when you copy the whole name then paste it again in
@@ -420,30 +326,17 @@ on_name_entry_text_changed_cb (EphySearchEngineRow *row,
    * if the name didn't actually change. This could toggle the entry as invalid
    * because the engine would already exist, so don't go any further in this case.
    */
-  if (g_strcmp0 (row->previous_name, new_name) == 0)
+  if (g_strcmp0 (ephy_search_engine_get_name (row->engine), new_name) == 0)
     return;
 
-  g_free (row->previous_name);
-  row->previous_name = g_strdup (new_name);
-
-  hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (row), new_name);
-
-  if (g_strcmp0 (new_name, EMPTY_NEW_SEARCH_ENGINE_NAME) == 0)
-    ephy_search_engine_list_box_set_can_add_engine (search_engine_list_box, FALSE);
-
   /* Name validation. */
   if (g_strcmp0 (new_name, "") == 0) {
     set_entry_as_invalid (name_entry, _("A name is required"));
-  } else if (search_engine_already_exists (row, new_name)) {
+  } else if (ephy_search_engine_manager_find_engine_by_name (row->manager, new_name)) {
     set_entry_as_invalid (name_entry, _("This search engine already exists"));
   } else {
     set_entry_as_valid (name_entry);
 
-    /* This allows the user to add new search engine again once it is renamed. */
-    if (g_strcmp0 (row->saved_name, EMPTY_NEW_SEARCH_ENGINE_NAME) == 0 &&
-        g_strcmp0 (new_name, EMPTY_NEW_SEARCH_ENGINE_NAME) != 0)
-      ephy_search_engine_list_box_set_can_add_engine (search_engine_list_box, TRUE);
-
     /* Let's not overwrite any existing bang, as that's likely not what is wanted.
      * For example when I wanted to rename my "wiktionary en" search engine that
      * had the !wte bang, it replaced the bang with !we, which is the one for
@@ -453,73 +346,47 @@ on_name_entry_text_changed_cb (EphySearchEngineRow *row,
     if (g_strcmp0 (gtk_entry_get_text (GTK_ENTRY (row->bang_entry)), "") == 0)
       update_bang_for_name (row, new_name);
 
-    ephy_search_engine_manager_rename (row->manager,
-                                       row->saved_name,
-                                       new_name);
-    g_free (row->saved_name);
-    row->saved_name = g_strdup (new_name);
+    ephy_search_engine_set_name (row->engine, new_name);
   }
 }
 
 static void
-on_radio_button_clicked_cb (EphySearchEngineRow *row,
-                            GtkButton           *button)
+on_radio_button_active_changed_cb (EphySearchEngineRow *self,
+                                   GParamSpec          *pspec,
+                                   GtkButton           *button)
 {
-  /* This avoids having some random engines being set as default when adding a new row,
-   * since when it default initialize the "active" property to %FALSE on object construction,
-   * it records a "clicked" signal
-   */
-  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
-    ephy_search_engine_manager_set_default_engine (row->manager, row->saved_name);
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)) &&
+      /* Avoid infinite loop between this callback and on_default_engine_changed_cb() */
+      ephy_search_engine_manager_get_default_engine (self->manager) != self->engine)
+    ephy_search_engine_manager_set_default_engine (self->manager, self->engine);
+}
+
+static void
+on_default_engine_changed_cb (EphySearchEngineManager *manager,
+                              GParamSpec              *pspec,
+                              EphySearchEngineRow     *self)
+{
+  if (ephy_search_engine_manager_get_default_engine (manager) == self->engine)
+    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->radio_button), TRUE);
 }
 
 static void
 on_remove_button_clicked_cb (EphySearchEngineRow *row,
                              GtkButton           *button)
 {
-  EphySearchEngineRow *top_row;
-  g_autofree char *default_engine = ephy_search_engine_manager_get_default_engine (row->manager);
-  GtkListBox *parent_list_box = GTK_LIST_BOX (gtk_widget_get_parent (GTK_WIDGET (row)));
-
-  /* Temporarly ref the row, as we'll remove it from its parent container
-   * but will still use some struct members of it.
-   */
-  g_object_ref (row);
-
-  ephy_search_engine_manager_delete_engine (row->manager,
-                                            row->saved_name);
-
   /* FIXME: this should be fixed in libhandy
    * Unexpand the row before removing it so the styling isn't broken.
    * See the checked-expander-row-previous-sibling style class in HdyExpanderRow documentation.
    */
   hdy_expander_row_set_expanded (HDY_EXPANDER_ROW (row), FALSE);
-  if (!search_engine_already_exists (row, row->saved_name))
-    ephy_search_engine_list_box_set_can_add_engine (EPHY_SEARCH_ENGINE_LIST_BOX (parent_list_box),
-                                                    TRUE);
-
-  gtk_container_remove (GTK_CONTAINER (parent_list_box), GTK_WIDGET (row));
-
-  top_row = EPHY_SEARCH_ENGINE_ROW (gtk_list_box_get_row_at_index (parent_list_box, 0));
-  /* Set an other row (the first one) as default search engine to replace this one (if it was the default 
one). */
-  if (g_strcmp0 (default_engine,
-                 row->saved_name) == 0)
-    ephy_search_engine_row_set_as_default (top_row);
 
-  if (gtk_list_box_get_row_at_index (parent_list_box, 2) == NULL)
-    gtk_widget_set_sensitive (top_row->remove_button, FALSE);
-
-  /* Drop the temporary reference */
-  g_object_unref (row);
+  ephy_search_engine_manager_delete_engine (row->manager, row->engine);
 }
 
 static void
 ephy_search_engine_row_finalize (GObject *object)
 {
-  EphySearchEngineRow *self = (EphySearchEngineRow *)object;
-
-  g_free (self->saved_name);
-  g_free (self->previous_name);
+  /* EphySearchEngineRow *self = (EphySearchEngineRow *)object; */
 
   G_OBJECT_CLASS (ephy_search_engine_row_parent_class)->finalize (object);
 }
@@ -533,42 +400,65 @@ ephy_search_engine_row_set_property (GObject      *object,
   EphySearchEngineRow *self = EPHY_SEARCH_ENGINE_ROW (object);
 
   switch (prop_id) {
-    case PROP_SEARCH_ENGINE_NAME:
-      g_free (self->saved_name);
-      self->saved_name = g_value_dup_string (value);
-      g_free (self->previous_name);
-      self->previous_name = g_value_dup_string (value);
+    case PROP_SEARCH_ENGINE:
+      self->engine = g_value_get_object (value);
+      break;
+    case PROP_MANAGER:
+      self->manager = g_value_get_object (value);
       break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
 }
 
+static void
+on_manager_items_changed_cb (EphySearchEngineManager *manager,
+                             guint                    position,
+                             guint                    removed,
+                             guint                    added,
+                             EphySearchEngineRow     *self)
+{
+  guint n_items = g_list_model_get_n_items (G_LIST_MODEL (manager));
+
+  /* We don't allow removing the engine if it's the last one, as it
+   * doesn't make sense at all and just too much relies on having a
+   * search engine available.
+   */
+  gtk_widget_set_sensitive (self->remove_button, n_items > 1);
+}
+
 static void
 on_ephy_search_engine_row_constructed (GObject *object)
 {
   EphySearchEngineRow *self = EPHY_SEARCH_ENGINE_ROW (object);
-  g_autofree char *default_search_engine_name = ephy_search_engine_manager_get_default_engine 
(self->manager);
 
-  g_assert (self->saved_name != NULL);
-  g_assert (g_strcmp0 (self->previous_name, self->saved_name) == 0);
+  g_assert (self->engine != NULL);
+  g_assert (self->manager != NULL);
 
-  gtk_entry_set_text (GTK_ENTRY (self->name_entry), self->saved_name);
-  hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), self->saved_name);
+  gtk_entry_set_text (GTK_ENTRY (self->name_entry), ephy_search_engine_get_name (self->engine));
+
+  /* We can't directly bind that in the UI file because there's issues with
+   * properties bindings that involve the root widget (the <template> root one).
+   */
+  g_object_bind_property (self->name_entry, "text",
+                          HDY_PREFERENCES_ROW (self), "title",
+                          G_BINDING_SYNC_CREATE | G_BINDING_DEFAULT);
 
   gtk_entry_set_text (GTK_ENTRY (self->address_entry),
-                      ephy_search_engine_manager_get_address (self->manager, self->saved_name));
+                      ephy_search_engine_get_url (self->engine));
   gtk_entry_set_text (GTK_ENTRY (self->bang_entry),
-                      ephy_search_engine_manager_get_bang (self->manager, self->saved_name));
-
-  /* Tick the radio button if it's the default search engine. */
-  if (g_strcmp0 (self->saved_name, default_search_engine_name) == 0)
-    ephy_search_engine_row_set_as_default (self);
+                      ephy_search_engine_get_bang (self->engine));
 
   g_signal_connect_object (self->name_entry, "notify::text", G_CALLBACK (on_name_entry_text_changed_cb), 
self, G_CONNECT_SWAPPED);
   g_signal_connect_object (self->address_entry, "notify::text", G_CALLBACK 
(on_address_entry_text_changed_cb), self, G_CONNECT_SWAPPED);
   g_signal_connect_object (self->bang_entry, "notify::text", G_CALLBACK (on_bang_entry_text_changed_cb), 
self, G_CONNECT_SWAPPED);
 
+  on_manager_items_changed_cb (self->manager, 0, 0, g_list_model_get_n_items (G_LIST_MODEL (self->manager)), 
self);
+  g_signal_connect_object (self->manager, "items-changed", G_CALLBACK (on_manager_items_changed_cb), self, 
0);
+
+  on_default_engine_changed_cb (self->manager, NULL, self);
+  g_signal_connect_object (self->manager, "notify::default-engine", G_CALLBACK 
(on_default_engine_changed_cb), self, 0);
+
   G_OBJECT_CLASS (ephy_search_engine_row_parent_class)->constructed (object);
 }
 
@@ -582,11 +472,16 @@ ephy_search_engine_row_class_init (EphySearchEngineRowClass *klass)
   object_class->set_property = ephy_search_engine_row_set_property;
   object_class->constructed = on_ephy_search_engine_row_constructed;
 
-  properties[PROP_SEARCH_ENGINE_NAME] = g_param_spec_string ("search-engine-name",
-                                                             "search-engine-name",
-                                                             "The name of the search engine",
-                                                             NULL,
-                                                             G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | 
G_PARAM_STATIC_STRINGS);
+  properties[PROP_SEARCH_ENGINE] = g_param_spec_object ("search-engine",
+                                                        "search-engine",
+                                                        "The search engine that this row should show and 
allow to edit.",
+                                                        EPHY_TYPE_SEARCH_ENGINE,
+                                                        G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | 
G_PARAM_STATIC_STRINGS);
+  properties[PROP_MANAGER] = g_param_spec_object ("manager",
+                                                  "manager",
+                                                  "The search engine manager that manages @search-engine.",
+                                                  EPHY_TYPE_SEARCH_ENGINE_MANAGER,
+                                                  G_PARAM_CONSTRUCT_ONLY | G_PARAM_WRITABLE | 
G_PARAM_STATIC_STRINGS);
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/epiphany/gtk/search-engine-row.ui");
@@ -597,14 +492,12 @@ ephy_search_engine_row_class_init (EphySearchEngineRowClass *klass)
   gtk_widget_class_bind_template_child (widget_class, EphySearchEngineRow, bang_entry);
   gtk_widget_class_bind_template_child (widget_class, EphySearchEngineRow, remove_button);
 
-  gtk_widget_class_bind_template_callback (widget_class, on_radio_button_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, on_radio_button_active_changed_cb);
   gtk_widget_class_bind_template_callback (widget_class, on_remove_button_clicked_cb);
 }
 
 static void
 ephy_search_engine_row_init (EphySearchEngineRow *self)
 {
-  self->manager = ephy_embed_shell_get_search_engine_manager (ephy_embed_shell_get_default ());
-
   gtk_widget_init_template (GTK_WIDGET (self));
 }
diff --git a/src/preferences/ephy-search-engine-row.h b/src/preferences/ephy-search-engine-row.h
index 4e5da5555..2257a9c30 100644
--- a/src/preferences/ephy-search-engine-row.h
+++ b/src/preferences/ephy-search-engine-row.h
@@ -22,6 +22,7 @@
 #pragma once
 
 #include <handy.h>
+#include "ephy-search-engine-manager.h"
 
 G_BEGIN_DECLS
 
@@ -29,12 +30,11 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (EphySearchEngineRow, ephy_search_engine_row, EPHY, SEARCH_ENGINE_ROW, HdyExpanderRow)
 
-EphySearchEngineRow *ephy_search_engine_row_new                    (const char *search_engine_name);
-GtkListBoxSortFunc   ephy_search_engine_row_get_sort_func          (void);
+EphySearchEngineRow *ephy_search_engine_row_new                    (EphySearchEngine        *engine,
+                                                                    EphySearchEngineManager *manager);
 void                 ephy_search_engine_row_set_can_remove         (EphySearchEngineRow *self,
                                                                     gboolean can_remove);
 void                 ephy_search_engine_row_set_radio_button_group (EphySearchEngineRow *self,
                                                                     GtkRadioButton      *radio_button_group);
-void                 ephy_search_engine_row_set_as_default         (EphySearchEngineRow *self);
 
 G_END_DECLS
diff --git a/src/resources/gtk/search-engine-listbox.ui b/src/resources/gtk/search-engine-listbox.ui
index 094819bb5..89091aea3 100644
--- a/src/resources/gtk/search-engine-listbox.ui
+++ b/src/resources/gtk/search-engine-listbox.ui
@@ -5,20 +5,6 @@
     <property name="visible">True</property>
     <property name="selection-mode">none</property>
     <signal name="row-activated" handler="on_add_search_engine_row_clicked_cb"/>
-    <child>
-      <object class="GtkListBoxRow" id="add_search_engine_row">
-        <property name="visible">True</property>
-        <property name="activatable">True</property>
-        <property name="height-request">50</property>
-        <child>
-          <object class="GtkLabel">
-            <property name="visible">True</property>
-            <property name="label" translatable="yes">A_dd Search Engine…</property>
-            <property name="use-underline">True</property>
-          </object>
-        </child>
-      </object>
-    </child>
     <style>
       <class name="content"/>
     </style>
diff --git a/src/resources/gtk/search-engine-row.ui b/src/resources/gtk/search-engine-row.ui
index 2656603bf..fa41f5f7b 100644
--- a/src/resources/gtk/search-engine-row.ui
+++ b/src/resources/gtk/search-engine-row.ui
@@ -8,7 +8,7 @@
         <property name="visible">True</property>
         <property name="valign">center</property>
         <property name="tooltip-text" translatable="yes">Selects the default search engine</property>
-        <signal name="clicked" handler="on_radio_button_clicked_cb" object="EphySearchEngineRow" 
swapped="yes"/>
+        <signal name="notify::active" handler="on_radio_button_active_changed_cb" 
object="EphySearchEngineRow" swapped="yes"/>
       </object>
     </child>
     <child>
diff --git a/src/search-provider/ephy-search-provider.c b/src/search-provider/ephy-search-provider.c
index fdbb37ef5..305d31d48 100644
--- a/src/search-provider/ephy-search-provider.c
+++ b/src/search-provider/ephy-search-provider.c
@@ -277,25 +277,10 @@ launch_search (EphySearchProvider  *self,
                guint                timestamp)
 {
   g_autofree char *search_string = NULL;
-  g_autofree char *query_param = NULL;
   g_autofree char *effective_url = NULL;
-  const char *address_search;
-  EphyEmbedShell *shell;
-  EphySearchEngineManager *search_engine_manager;
-
-  shell = ephy_embed_shell_get_default ();
-  search_engine_manager = ephy_embed_shell_get_search_engine_manager (shell);
-  address_search = ephy_search_engine_manager_get_default_search_address (search_engine_manager);
 
   search_string = g_strjoinv (" ", terms);
-  query_param = soup_form_encode ("q", search_string, NULL);
-
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wformat-nonliteral"
-  /* Format string under control of user input... but gsettings is trusted input. */
-  /* + 2 here is getting rid of 'q=' */
-  effective_url = g_strdup_printf (address_search, query_param + 2);
-#pragma GCC diagnostic pop
+  effective_url = ephy_embed_utils_autosearch_address (search_string);
 
   launch_uri (effective_url, timestamp);
 }
diff --git a/tests/ephy-web-view-test.c b/tests/ephy-web-view-test.c
index 3c359237e..1cc18770b 100644
--- a/tests/ephy-web-view-test.c
+++ b/tests/ephy-web-view-test.c
@@ -320,37 +320,35 @@ verify_normalize_or_autosearch_urls (EphyWebView               *view,
 static void
 test_ephy_web_view_normalize_or_autosearch (void)
 {
-  char *default_engine;
   EphyWebView *view;
   EphySearchEngineManager *manager;
   EphyEmbedShell *shell;
-
+  EphySearchEngine *default_engine;
+  g_autoptr (EphySearchEngine) test_engine = NULL;
 
   view = EPHY_WEB_VIEW (ephy_web_view_new ());
 
   shell = ephy_embed_shell_get_default ();
   manager = ephy_embed_shell_get_search_engine_manager (shell);
 
-
   default_engine = ephy_search_engine_manager_get_default_engine (manager);
-  ephy_search_engine_manager_add_engine (manager,
-                                         "org.gnome.Epiphany.EphyWebViewTest",
-                                         "http://duckduckgo.com/?q=%s&t=epiphany";,
-                                         "");
-  g_assert_true (ephy_search_engine_manager_set_default_engine (manager, 
"org.gnome.Epiphany.EphyWebViewTest"));
+  test_engine = g_object_new (EPHY_TYPE_SEARCH_ENGINE,
+                              "name", "org.gnome.Epiphany.EphyWebViewTest",
+                              "url", "http://duckduckgo.com/?q=%s&t=epiphany";,
+                              NULL);
+  ephy_search_engine_manager_add_engine (manager, test_engine);
+  ephy_search_engine_manager_set_default_engine (manager, test_engine);
+  g_assert_true (ephy_search_engine_manager_get_default_engine (manager) == test_engine);
   verify_normalize_or_autosearch_urls (view, normalize_or_autosearch_test_ddg, G_N_ELEMENTS 
(normalize_or_autosearch_test_ddg));
 
-  ephy_search_engine_manager_modify_engine (manager,
-                                            "org.gnome.Epiphany.EphyWebViewTest",
-                                            "http://www.google.com/?q=%s";,
-                                            "");
+  ephy_search_engine_set_url (test_engine, "http://www.google.com/?q=%s";);
 
   verify_normalize_or_autosearch_urls (view, normalize_or_autosearch_test_google, G_N_ELEMENTS 
(normalize_or_autosearch_test_google));
 
-  ephy_search_engine_manager_delete_engine (manager, "org.gnome.Epiphany.EphyWebViewTest");
+  ephy_search_engine_manager_delete_engine (manager, test_engine);
 
-  g_assert_true (ephy_search_engine_manager_set_default_engine (manager, default_engine));
-  g_free (default_engine);
+  ephy_search_engine_manager_set_default_engine (manager, default_engine);
+  g_assert_true (ephy_search_engine_manager_get_default_engine (manager) == default_engine);
   g_object_unref (g_object_ref_sink (view));
 }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]