Blob Blame History Raw
/* test-suggestion.c
 *
 * Copyright (C) 2017 Christian Hergert <chergert@redhat.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/>.
 */

#include <string.h>
#include <dazzle.h>

/*
 * Most of this is exactly how you SHOULD NOT write a web browser
 * shell. It's just dummy code to test the suggestions widget.
 * Think for yourself before copying any of this code.
 */

typedef struct
{
  const gchar *icon_name;
  const gchar *url;
  const gchar *title;
  const gchar *suffix;
} DemoData;

static DzlFuzzyMutableIndex *search_index;
static const DemoData demo_data[] = {
  { "web-browser-symbolic", "https://twitter.com", "Twitter", "twitter.com" },
  { "web-browser-symbolic", "https://facebook.com", "Facebook", "facebook.com" },
  { "web-browser-symbolic", "https://google.com", "Google", "google.com" },
  { "web-browser-symbolic", "https://images.google.com", "Google Images", "images.google.com" },
  { "web-browser-symbolic", "https://news.ycombinator.com", "Hacker News", "news.ycombinator.com" },
  { "web-browser-symbolic", "https://reddit.com/r/gnome", "GNOME Desktop Environment", "reddit.com/r/gnome" },
  { "web-browser-symbolic", "https://reddit.com/r/linux", "Linux, GNU/Linux, free software", "reddit.com/r/linux" },
  { "web-browser-symbolic", "https://wiki.gnome.org", "GNOME Wiki", "wiki.gnome.org" },
  { "web-browser-symbolic", "https://gnome.org", "GNOME", "gnome.org" },
  { "web-browser-symbolic", "https://planet.gnome.org", "Planet GNOME", "planet.gnome.org" },
  { "web-browser-symbolic", "https://wiki.gnome.org/Apps/Builder", "GNOME Builder", "wiki.gnome.org/Apps/Builder" },
};

static void
take_item (GListStore    *store,
           DzlSuggestion *suggestion)
{
  g_list_store_append (store, suggestion);
  g_object_unref (suggestion);
}

static gboolean
is_a_url (const gchar *str)
{
  /* you obviously want something better */

  if (strstr (str, ".com") ||
      strstr (str, ".net") ||
      strstr (str, ".org") ||
      strstr (str, ".io") ||
      strstr (str, ".ly"))
    return TRUE;

  return FALSE;
}

static gint
compare_match (gconstpointer a,
               gconstpointer b)
{
  const DzlFuzzyMutableIndexMatch *match_a = a;
  const DzlFuzzyMutableIndexMatch *match_b = b;

  if (match_a->score < match_b->score)
    return 1;
  else if (match_a->score > match_b->score)
    return -1;
  else
    return 0;
}

static gchar *
suggest_suffix (DzlSuggestion  *suggestion,
                const gchar    *typed_text,
                const DemoData *data)
{
  //g_print ("Suffix: Typed_Text=%s\n", typed_text);

  if (g_str_has_prefix (data->suffix, typed_text))
    return g_strdup (data->suffix + strlen (typed_text));

  return NULL;
}

static gchar *
replace_typed_text (DzlSuggestion  *suggestion,
                    const gchar    *typed_text,
                    const DemoData *data)
{
  return g_strdup (data->url);
}

static GListModel *
create_search_results (const gchar *full_query,
                       const gchar *query)
{
  GListStore *store = g_list_store_new (DZL_TYPE_SUGGESTION);
  g_autoptr(GArray) matches = NULL;
  g_autofree gchar *search_url = NULL;
  g_autofree gchar *with_slashes = g_strdup_printf ("://%s", query);
  gboolean exact = FALSE;

  matches = dzl_fuzzy_mutable_index_match (search_index, query, 20);

  g_array_sort (matches, compare_match);

  for (guint i = 0; i < matches->len; i++)
    {
      const DzlFuzzyMutableIndexMatch *match = &g_array_index (matches, DzlFuzzyMutableIndexMatch, i);
      const DemoData *data = match->value;
      g_autofree gchar *markup = NULL;
      DzlSuggestion *item;

      markup = dzl_fuzzy_highlight (data->url, query, FALSE);

      if (g_str_has_suffix (data->url, with_slashes))
        exact = TRUE;

      item = g_object_new (DZL_TYPE_SUGGESTION,
                           "id", data->url,
                           "icon-name", data->icon_name,
                           "title", markup,
                           "subtitle", data->title,
                           NULL);
      g_signal_connect (item, "suggest-suffix", G_CALLBACK (suggest_suffix), (gpointer)data);
      g_signal_connect (item, "replace-typed-text", G_CALLBACK (replace_typed_text), (gpointer)data);
      take_item (store, item);
    }

  if (!exact && is_a_url (full_query))
    {
      g_autofree gchar *url = g_strdup_printf ("http://%s", full_query);
      take_item (store,
                 g_object_new (DZL_TYPE_SUGGESTION,
                               "id", url,
                               "icon-name", NULL,
                               "title", query,
                               "subtitle", NULL,
                               NULL));
    }

  search_url = g_strdup_printf ("https://www.google.com/search?q=%s", full_query);
  take_item (store,
             g_object_new (DZL_TYPE_SUGGESTION,
                           "id", search_url,
                           "title", full_query,
                           "subtitle", "Google Search",
                           "icon-name", "edit-find-symbolic",
                           NULL));

  return G_LIST_MODEL (store);
}

static gboolean
key_press (GtkWidget   *widget,
           GdkEventKey *key,
           GtkEntry    *param)
{
  if (key->keyval == GDK_KEY_l && (key->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
    {
      gtk_widget_grab_focus (GTK_WIDGET (param));
      gtk_editable_select_region (GTK_EDITABLE (param), 0, -1);
    }
  else if (key->keyval == GDK_KEY_w && (key->state & GDK_CONTROL_MASK) == GDK_CONTROL_MASK)
    gtk_window_close (GTK_WINDOW (widget));

  return FALSE;
}

static void
search_changed (DzlSuggestionEntry *entry,
                gpointer            user_data)
{
  g_autoptr(GListModel) model = NULL;
  GString *str = g_string_new (NULL);
  const gchar *text;

  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));

  text = dzl_suggestion_entry_get_typed_text (entry);

  for (const gchar *iter = text; *iter; iter = g_utf8_next_char (iter))
    {
      gunichar ch = g_utf8_get_char (iter);

      if (!g_unichar_isspace (ch))
        g_string_append_unichar (str, ch);
    }

  if (str->len)
    model = create_search_results (text, str->str);

  dzl_suggestion_entry_set_model (entry, model);
}

static void
suggestion_activated (DzlSuggestionEntry *entry,
                      DzlSuggestion      *suggestion,
                      gpointer            user_data)
{
  const gchar *uri = dzl_suggestion_get_id (suggestion);

  g_print ("Activated suggestion: %s\n", uri);

  gtk_entry_set_text (GTK_ENTRY (entry), uri);
}

static void
load_css (void)
{
  g_autoptr(GtkCssProvider) provider = NULL;

  provider = dzl_css_provider_new ("resource:///org/gnome/dazzle/themes");
  gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
                                             GTK_STYLE_PROVIDER (provider),
                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

int
main (gint   argc,
      gchar *argv[])
{
  GtkWidget *window;
  GtkWidget *header;
  GtkWidget *entry;
  GtkWidget *box;
  GtkWidget *button;
  GtkWidget *scroller;

  gtk_init (&argc, &argv);

  load_css ();

  search_index = dzl_fuzzy_mutable_index_new (FALSE);

  for (guint i = 0; i < G_N_ELEMENTS (demo_data); i++)
    {
      const DemoData *data = &demo_data[i];

      dzl_fuzzy_mutable_index_insert (search_index, data->url, (gpointer)data);
    }

  window = g_object_new (GTK_TYPE_WINDOW,
                         "default-width", 1100,
                         "default-height", 600,
                         NULL);
  header = g_object_new (GTK_TYPE_HEADER_BAR,
                         "show-close-button", TRUE,
                         "visible", TRUE,
                         NULL);

  scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
                           "child", g_object_new (GTK_TYPE_TEXT_VIEW,
                                                  "visible", TRUE,
                                                  NULL),
                           "expand", TRUE,
                           "visible", TRUE,
                           NULL);
  gtk_container_add (GTK_CONTAINER (window), scroller);

  box = g_object_new (GTK_TYPE_BOX,
                      "orientation", GTK_ORIENTATION_HORIZONTAL,
                      "visible", TRUE,
                      "spacing", 0,
                      NULL);
  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (box)), "linked");

  entry = g_object_new (DZL_TYPE_SUGGESTION_ENTRY,
                        "halign", GTK_ALIGN_CENTER,
                        "hexpand", FALSE,
                        "max-width-chars", 55,
                        "visible", TRUE,
                        "width-chars", 30,
                        NULL);
  dzl_suggestion_entry_set_position_func (DZL_SUGGESTION_ENTRY (entry),
                                          dzl_suggestion_entry_window_position_func,
                                          NULL, NULL);
  gtk_box_set_center_widget (GTK_BOX (box), entry);
  g_signal_connect (entry, "changed", G_CALLBACK (search_changed), NULL);
  g_signal_connect (entry, "suggestion-activated", G_CALLBACK (suggestion_activated), NULL);

  button = g_object_new (GTK_TYPE_BUTTON,
                         "halign", GTK_ALIGN_START,
                         "hexpand", FALSE,
                         "visible", TRUE,
                         "child", g_object_new (GTK_TYPE_IMAGE,
                                                "visible", TRUE,
                                                "icon-name", "web-browser-symbolic",
                                                NULL),
                         NULL);
  gtk_box_pack_end (GTK_BOX (box), button, FALSE, FALSE, 0);

  gtk_window_set_titlebar (GTK_WINDOW (window), header);
  gtk_header_bar_set_custom_title (GTK_HEADER_BAR (header), box);

  g_signal_connect (window, "delete-event", gtk_main_quit, NULL);
  g_signal_connect (window, "key-press-event", G_CALLBACK (key_press), entry);
  gtk_window_present (GTK_WINDOW (window));

  gtk_main ();

  return 0;
}