/* GTK Demo
 *
 * GTK Demo is a collection of useful examples to demonstrate
 * GTK widgets and features. It is a useful example in itself.
 *
 * You can select examples in the sidebar or search for them by
 * typing a search term. Double-clicking or hitting the “Run” button
 * will run the demo. The source code and other resources used in the
 * demo are shown in this area.
 *
 * You can also use the GTK Inspector, available from the menu on the
 * top right, to poke at the running demos, and see how they are put
 * together.
 */
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "config.h"
#include <gtk/gtk.h>
#include <gmodule.h>
#include <glib/gstdio.h>
#include <glib/gi18n.h>

#include "demos.h"
#include "fontify.h"

#include "profile_conf.h"

static GtkWidget *info_view;
static GtkWidget *source_view;

static char *current_file = NULL;

static GtkWidget *notebook;
static GtkSingleSelection *selection;
static GtkWidget *toplevel;
static GtkWidget *search_bar;
static GtkWidget *search_entry;
static char **search_needle;

typedef struct _GtkDemo GtkDemo;
struct _GtkDemo
{
  GObject parent_instance;

  const char *name;
  const char *title;
  const char **keywords;
  const char *filename;
  GDoDemoFunc func;
  GListModel *children_model;
};

enum {
  PROP_0,
  PROP_FILENAME,
  PROP_NAME,
  PROP_TITLE,
  PROP_KEYWORDS,

  N_PROPS
};

# define GTK_TYPE_DEMO (gtk_demo_get_type ())
G_DECLARE_FINAL_TYPE (GtkDemo, gtk_demo, GTK, DEMO, GObject);

G_DEFINE_TYPE (GtkDemo, gtk_demo, G_TYPE_OBJECT);
static GParamSpec *properties[N_PROPS] = { NULL, };

static void
gtk_demo_get_property (GObject    *object,
                       guint       property_id,
                       GValue     *value,
                       GParamSpec *pspec)
{
  GtkDemo *self = GTK_DEMO (object);

  switch (property_id)
    {
    case PROP_FILENAME:
      g_value_set_string (value, self->filename);
      break;

    case PROP_NAME:
      g_value_set_string (value, self->name);
      break;

    case PROP_TITLE:
      g_value_set_string (value, self->title);
      break;

    case PROP_KEYWORDS:
      g_value_set_boxed (value, self->keywords);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void gtk_demo_class_init (GtkDemoClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->get_property = gtk_demo_get_property;

  properties[PROP_FILENAME] =
    g_param_spec_string ("filename",
                         NULL, NULL,
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_NAME);
  properties[PROP_NAME] =
    g_param_spec_string ("name",
                         NULL, NULL,
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_NAME);
  properties[PROP_TITLE] =
    g_param_spec_string ("title",
                         NULL, NULL,
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_NAME);
  properties[PROP_KEYWORDS] =
    g_param_spec_string ("keywords",
                         NULL, NULL,
                         NULL,
                         G_PARAM_READABLE | G_PARAM_STATIC_NAME);

  g_object_class_install_properties (gobject_class, N_PROPS, properties);
}

static void gtk_demo_init (GtkDemo *self)
{
}

typedef struct _CallbackData CallbackData;
struct _CallbackData
{
  GtkTreeModel *model;
  GtkTreePath *path;
};

static gboolean
gtk_demo_run (GtkDemo   *self,
              GtkWidget *window)
{
  GtkWidget *result;

  if (!self->func)
    return FALSE;

  result = self->func (window);
  if (result == NULL)
    return FALSE;

  return TRUE;
}

static void
activate_about (GSimpleAction *action,
                GVariant      *parameter,
                gpointer       user_data)
{
  GtkApplication *app = user_data;
  char *version;
  char *os_name;
  char *os_version;
  GString *s;
  GFile *logo_file;
  GtkIconPaintable *logo;

  s = g_string_new ("");

  os_name = g_get_os_info (G_OS_INFO_KEY_NAME);
  os_version = g_get_os_info (G_OS_INFO_KEY_VERSION_ID);
  if (os_name && os_version)
    g_string_append_printf (s, "OS\t%s %s\n\n", os_name, os_version);
  g_string_append (s, "System libraries\n");
  g_string_append_printf (s, "\tGLib\t%d.%d.%d\n",
                          glib_major_version,
                          glib_minor_version,
                          glib_micro_version);
  g_string_append_printf (s, "\tPango\t%s\n",
                          pango_version_string ());
  g_string_append_printf (s, "\tGTK \t%d.%d.%d\n",
                          gtk_get_major_version (),
                          gtk_get_minor_version (),
                          gtk_get_micro_version ());
  g_string_append_printf (s, "\nA link can appear here: <http://www.gtk.org>");

  version = g_strdup_printf ("%s%s%s\nRunning against GTK %d.%d.%d",
                             PACKAGE_VERSION,
                             g_strcmp0 (PROFILE, "devel") == 0 ? "-" : "",
                             g_strcmp0 (PROFILE, "devel") == 0 ? VCS_TAG : "",
                             gtk_get_major_version (),
                             gtk_get_minor_version (),
                             gtk_get_micro_version ());

  logo_file = g_file_new_for_uri ("resource:///org/gtk/Demo4/icons/scalable/apps/org.gtk.Demo4.svg");
  logo = gtk_icon_paintable_new_for_file (logo_file, 64, 1);
  gtk_show_about_dialog (GTK_WINDOW (gtk_application_get_active_window (app)),
                         "program-name", g_strcmp0 (PROFILE, "devel") == 0
                                         ? "GTK Demo (Development)"
                                         : "GTK Demo",
                         "version", version,
                         "copyright", "© 1997—2024 The GTK Team",
                         "license-type", GTK_LICENSE_LGPL_2_1,
                         "website", "http://www.gtk.org",
                         "comments", "Program to demonstrate GTK widgets",
                         "authors", (const char *[]) { "The GTK Team", NULL },
                         "logo", logo,
                         "title", "About GTK Demo",
                         "system-information", s->str,
                         NULL);
  g_object_unref (logo);
  g_object_unref (logo_file);

  g_string_free (s, TRUE);
  g_free (version);
  g_free (os_name);
  g_free (os_version);
}

static void
activate_quit (GSimpleAction *action,
               GVariant      *parameter,
               gpointer       user_data)
{
  GtkApplication *app = user_data;
  GtkWidget *win;
  GList *list, *next;

  list = gtk_application_get_windows (app);
  while (list)
    {
      win = list->data;
      next = list->next;

      gtk_window_destroy (GTK_WINDOW (win));

      list = next;
    }
}

static void
activate_inspector (GSimpleAction *action,
                    GVariant      *parameter,
                    gpointer       user_data)
{
  gtk_window_set_interactive_debugging (TRUE);
}

static void
activate_run (GSimpleAction *action,
              GVariant      *parameter,
              gpointer       window)
{
  GtkTreeListRow *row = gtk_single_selection_get_selected_item (selection);
  GtkDemo *demo = gtk_tree_list_row_get_item (row);

  gtk_demo_run (demo, window);
}

static GtkWidget *
display_image (const char *format,
               const char *resource,
               GtkWidget  *label)
{
  GtkWidget *sw, *image;

  image = gtk_picture_new_for_resource (resource);
  gtk_widget_set_halign (image, GTK_ALIGN_CENTER);
  gtk_widget_set_valign (image, GTK_ALIGN_CENTER);
  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), image);

  gtk_accessible_update_relation (GTK_ACCESSIBLE (image),
                                  GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
                                  -1);

  return sw;
}

static GtkWidget *
display_images (const char *format,
                const char *resource_dir,
                GtkWidget  *label)
{
  char **resources;
  GtkWidget *grid;
  GtkWidget *sw;
  GtkWidget *widget;
  guint i;

  resources = g_resources_enumerate_children (resource_dir, 0, NULL);
  if (resources == NULL)
    return NULL;

  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
                                  GTK_POLICY_NEVER,
                                  GTK_POLICY_AUTOMATIC);
  grid = gtk_flow_box_new ();
  gtk_flow_box_set_selection_mode (GTK_FLOW_BOX (grid), GTK_SELECTION_NONE);
  gtk_widget_set_valign (grid, GTK_ALIGN_START);
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), grid);

  for (i = 0; resources[i]; i++)
    {
      char *resource_name;
      GtkWidget *box;
      GtkWidget *image_label;

      resource_name = g_strconcat (resource_dir, "/", resources[i], NULL);

      image_label = gtk_label_new (resources[i]);
      widget = display_image (NULL, resource_name, image_label);
      box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
      gtk_box_append (GTK_BOX (box), widget);
      gtk_box_append (GTK_BOX (box), image_label);
      gtk_flow_box_insert (GTK_FLOW_BOX (grid), box, -1);

      g_free (resource_name);
    }

  g_strfreev (resources);

  gtk_label_set_label (GTK_LABEL (label), "Images");

  gtk_accessible_update_relation (GTK_ACCESSIBLE (grid),
                                  GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
                                  -1);

  return sw;
}

static GtkWidget *
display_text (const char *format,
              const char *resource,
              GtkWidget  *label)
{
  GtkTextBuffer *buffer;
  GtkWidget *textview, *sw;
  GBytes *bytes;
  const char *text;
  gsize len;

  bytes = g_resources_lookup_data (resource, 0, NULL);
  g_assert (bytes);
  text = g_bytes_get_data (bytes, &len);
  g_assert (g_utf8_validate (text, len, NULL));

  textview = gtk_text_view_new ();
  gtk_text_view_set_left_margin (GTK_TEXT_VIEW (textview), 20);
  gtk_text_view_set_right_margin (GTK_TEXT_VIEW (textview), 20);
  gtk_text_view_set_top_margin (GTK_TEXT_VIEW (textview), 20);
  gtk_text_view_set_bottom_margin (GTK_TEXT_VIEW (textview), 20);
  gtk_text_view_set_editable (GTK_TEXT_VIEW (textview), FALSE);
  gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (textview), FALSE);
  /* Make it a bit nicer for text. */
  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (textview), GTK_WRAP_WORD);
  gtk_text_view_set_pixels_above_lines (GTK_TEXT_VIEW (textview), 2);
  gtk_text_view_set_pixels_below_lines (GTK_TEXT_VIEW (textview), 2);
  gtk_text_view_set_monospace (GTK_TEXT_VIEW (textview), TRUE);

  buffer = gtk_text_buffer_new (NULL);
  gtk_text_buffer_set_text (buffer, text, len);
  g_bytes_unref (bytes);

  if (format)
    fontify (format, buffer);

  gtk_text_view_set_buffer (GTK_TEXT_VIEW (textview), buffer);

  gtk_accessible_update_relation (GTK_ACCESSIBLE (textview),
                                  GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
                                  -1);

  sw = gtk_scrolled_window_new ();
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
                                  GTK_POLICY_AUTOMATIC,
                                  GTK_POLICY_AUTOMATIC);
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), textview);

  return sw;
}

static GtkWidget *
display_video (const char *format,
               const char *resource,
               GtkWidget  *label)
{
  GtkWidget *video;

  video = gtk_video_new_for_resource (resource);
  gtk_video_set_loop (GTK_VIDEO (video), TRUE);

  gtk_accessible_update_relation (GTK_ACCESSIBLE (video),
                                  GTK_ACCESSIBLE_RELATION_LABELLED_BY, label, NULL,
                                  -1);

  return video;
}

static GtkWidget *
display_nothing (const char *resource)
{
  GtkWidget *widget;
  char *str;

  str = g_strdup_printf ("The contents of the resource at '%s' cannot be displayed", resource);
  widget = gtk_label_new (str);
  gtk_label_set_wrap (GTK_LABEL (widget), TRUE);

  g_free (str);

  return widget;
}

static struct {
  const char *extension;
  const char *format;
  GtkWidget * (* display_func) (const char *format,
                                const char *resource,
                                GtkWidget  *label);
} display_funcs[] = {
  { ".gif", NULL, display_image },
  { ".jpg", NULL, display_image },
  { ".png", NULL, display_image },
  { ".svg", NULL, display_image },
  { ".c", "c", display_text },
  { ".css", "css", display_text },
  { ".glsl", NULL, display_text },
  { ".h", "c", display_text },
  { ".txt", NULL, display_text },
  { ".ui", "xml", display_text },
  { ".webm", NULL, display_video },
  { "images/", NULL, display_images }
};

static void
add_data_tab (const char *demoname)
{
  char *resource_dir, *resource_name;
  char **resources;
  GtkWidget *widget, *label;
  guint i, j;

  resource_dir = g_strconcat ("/", demoname, NULL);
  resources = g_resources_enumerate_children (resource_dir, 0, NULL);
  if (resources == NULL)
    {
      g_free (resource_dir);
      return;
    }

  for (i = 0; resources[i]; i++)
    {
      resource_name = g_strconcat (resource_dir, "/", resources[i], NULL);

      for (j = 0; j < G_N_ELEMENTS (display_funcs); j++)
        {
          if (g_str_has_suffix (resource_name, display_funcs[j].extension))
            break;
        }

      label = gtk_label_new (resources[i]);

      if (j < G_N_ELEMENTS (display_funcs))
        widget = display_funcs[j].display_func (display_funcs[j].format,
                                                resource_name,
                                                label);
      else
        widget = display_nothing (resource_name);

      gtk_notebook_append_page (GTK_NOTEBOOK (notebook), widget, label);
      g_object_set (gtk_notebook_get_page (GTK_NOTEBOOK (notebook), widget),
                    "tab-expand", FALSE,
                    NULL);

      g_free (resource_name);
    }

  g_strfreev (resources);
  g_free (resource_dir);
}

static void
remove_data_tabs (void)
{
  int i;

  for (i = gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook)) - 1; i > 1; i--)
    gtk_notebook_remove_page (GTK_NOTEBOOK (notebook), i);
}

void
load_file (const char *demoname,
           const char *filename)
{
  GtkTextBuffer *info_buffer, *source_buffer;
  GtkTextIter start, end;
  char *resource_filename;
  GError *err = NULL;
  int state = 0;
  gboolean in_para = 0;
  char **lines;
  GBytes *bytes;
  int i;

  if (g_strcmp0 (current_file, filename) == 0)
    return;

  remove_data_tabs ();

  add_data_tab (demoname);

  g_free (current_file);
  current_file = g_strdup (filename);

  info_buffer = gtk_text_buffer_new (NULL);
  gtk_text_buffer_create_tag (info_buffer, "title",
                              "size", 18 * 1024,
                              "pixels-below-lines", 10,
                              NULL);

  source_buffer = gtk_text_buffer_new (NULL);

  gtk_text_buffer_begin_irreversible_action (info_buffer);
  gtk_text_buffer_begin_irreversible_action (source_buffer);

  resource_filename = g_strconcat ("/sources/", filename, NULL);
  bytes = g_resources_lookup_data (resource_filename, 0, &err);
  g_free (resource_filename);

  if (bytes == NULL)
    {
      g_warning ("Cannot open source for %s: %s", filename, err->message);
      g_error_free (err);
      return;
    }

  lines = g_strsplit (g_bytes_get_data (bytes, NULL), "\n", -1);
  g_bytes_unref (bytes);

  gtk_text_buffer_get_iter_at_offset (info_buffer, &start, 0);
  for (i = 0; lines[i] != NULL; i++)
    {
      char *p;
      char *q;
      char *r;

      /* Make sure \r is stripped at the end for the poor windows people */
      lines[i] = g_strchomp (lines[i]);

      p = lines[i];
      switch (state)
        {
        case 0:
          /* Reading title */
          while (*p == '/' || *p == '*' || g_ascii_isspace (*p))
            p++;
          r = p;
          while (*r != '\0')
            {
              while (*r != '/' && *r != ':' && *r != '\0')
                r++;
              if (*r == '/')
                {
                  r++;
                  p = r;
                }
              if (r[0] == ':' && r[1] == ':')
                *r = '\0';
            }
          q = p + strlen (p);
          while (q > p && g_ascii_isspace (*(q - 1)))
            q--;


          if (q > p)
            {
              int len_chars = g_utf8_pointer_to_offset (p, q);

              end = start;

              g_assert (strlen (p) >= q - p);
              gtk_text_buffer_insert (info_buffer, &end, p, q - p);
              start = end;

              gtk_text_iter_backward_chars (&start, len_chars);
              gtk_text_buffer_apply_tag_by_name (info_buffer, "title", &start, &end);

              start = end;

              while (*p && *p != '\n') p++;

              state++;
            }
          break;

        case 1:
          /* Reading body of info section */
          while (g_ascii_isspace (*p))
            p++;
          if (*p == '*' && *(p + 1) == '/')
            {
              gtk_text_buffer_get_iter_at_offset (source_buffer, &start, 0);
              state++;
            }
          else
            {
              int len;

              while (*p == '*' || g_ascii_isspace (*p))
                p++;

              len = strlen (p);
              while (g_ascii_isspace (*(p + len - 1)))
                len--;

              if (*p == '#')
                break;

              if (len > 0)
                {
                  if (in_para)
                    gtk_text_buffer_insert (info_buffer, &start, " ", 1);

                  g_assert (strlen (p) >= len);
                  gtk_text_buffer_insert (info_buffer, &start, p, len);
                  in_para = 1;
                }
              else
                {
                  gtk_text_buffer_insert (info_buffer, &start, "\n", 1);
                  in_para = 0;
                }
            }
          break;

        case 2:
          /* Skipping blank lines */
          while (g_ascii_isspace (*p))
            p++;

          if (!*p)
            break;

          p = lines[i];
          state++;
          G_GNUC_FALLTHROUGH;

        case 3:
          /* Reading program body */
          gtk_text_buffer_insert (source_buffer, &start, p, -1);
          if (lines[i+1] != NULL)
            gtk_text_buffer_insert (source_buffer, &start, "\n", 1);
          break;

        default:
          g_assert_not_reached ();
        }
    }

  g_strfreev (lines);

  fontify ("c", source_buffer);

  gtk_text_buffer_end_irreversible_action (source_buffer);
  gtk_text_view_set_buffer (GTK_TEXT_VIEW (source_view), source_buffer);
  g_object_unref (source_buffer);

  gtk_text_buffer_end_irreversible_action (info_buffer);
  gtk_text_view_set_buffer (GTK_TEXT_VIEW (info_view), info_buffer);
  g_object_unref (info_buffer);
}

static void
activate_cb (GtkWidget *widget,
             guint      position,
             gpointer   window)
{
  GListModel *model = G_LIST_MODEL (gtk_list_view_get_model (GTK_LIST_VIEW (widget)));
  GtkTreeListRow *row = g_list_model_get_item (model, position);
  GtkDemo *demo = gtk_tree_list_row_get_item (row);

  gtk_demo_run (demo, window);

  g_object_unref (row);
}

static void
selection_cb (GtkSingleSelection *sel,
              GParamSpec         *pspec,
              gpointer            user_data)
{
  GtkTreeListRow *row = gtk_single_selection_get_selected_item (sel);
  GtkDemo *demo;
  GAction *action;

  gtk_widget_set_sensitive (GTK_WIDGET (notebook), !!row);

  if (!row)
    {
      gtk_window_set_title (GTK_WINDOW (toplevel), "No match");

      return;
    }

  demo = gtk_tree_list_row_get_item (row);

  if (demo->filename)
    load_file (demo->name, demo->filename);

  action = g_action_map_lookup_action (G_ACTION_MAP (toplevel), "run");
  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), demo->func != NULL);

  gtk_window_set_title (GTK_WINDOW (toplevel), demo->title);
}

static gboolean
filter_demo (GtkDemo *demo)
{
  int i;

  /* Show only if the name matches every needle */
  for (i = 0; search_needle[i]; i++)
    {
      if (!demo->title)
        return FALSE;

      if (g_str_match_string (search_needle[i], demo->title, TRUE))
        continue;

      if (demo->keywords)
        {
          int j;
          gboolean found = FALSE;

          for (j = 0; !found && demo->keywords[j]; j++)
            {
              if (strstr (demo->keywords[j], search_needle[i]))
                found = TRUE;
            }

          if (found)
            continue;
        }

      return FALSE;
    }

  return TRUE;
}

static gboolean
demo_filter_by_name (gpointer item,
                     gpointer user_data)
{
  GtkTreeListRow *row = item;
  GListModel *children;
  GtkDemo *demo;
  guint i, n;
  GtkTreeListRow *parent;

  /* Show all items if search is empty */
  if (!search_needle || !search_needle[0] || !*search_needle[0])
    return TRUE;

  g_assert (GTK_IS_TREE_LIST_ROW (row));
  g_assert (GTK_IS_FILTER_LIST_MODEL (user_data));

  /* Show a row if itself of any parent matches */
  for (parent = row; parent; parent = gtk_tree_list_row_get_parent (parent))
    {
      demo = gtk_tree_list_row_get_item (parent);
      g_assert (GTK_IS_DEMO (demo));

      if (filter_demo (demo))
        return TRUE;
    }

  /* Show a row if any child matches */
  children = gtk_tree_list_row_get_children (row);
  if (children)
    {
      n = g_list_model_get_n_items (children);
      for (i = 0; i < n; i++)
        {
          demo = g_list_model_get_item (children, i);
          g_assert (GTK_IS_DEMO (demo));

          if (filter_demo (demo))
            {
              g_object_unref (demo);
              return TRUE;
            }
          g_object_unref (demo);
        }
    }

  return FALSE;
}

static void
demo_search_changed_cb (GtkSearchEntry *entry,
                        GtkFilter      *filter)
{
  char *text;

  g_assert (GTK_IS_SEARCH_ENTRY (entry));
  g_assert (GTK_IS_FILTER (filter));

  text = gtk_editable_get_complete_text (GTK_EDITABLE (entry));

  g_clear_pointer (&search_needle, g_strfreev);

  if (text && *text)
    search_needle = g_str_tokenize_and_fold (text, NULL, NULL);

  gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT);
  g_free (text);
}

static gboolean
demo_can_run (GtkWidget  *window,
              const char *name)
{
  return TRUE;
}

static GListModel *
create_demo_model (GtkWidget *window)
{
  GListStore *store = g_list_store_new (GTK_TYPE_DEMO);
  DemoData *demo = gtk_demos;
  GtkDemo *d;

  gtk_widget_realize (window);

  d = GTK_DEMO (g_object_new (GTK_TYPE_DEMO, NULL));
  d->name = "main";
  d->title = "GTK Demo";
  d->keywords = NULL;
  d->filename = "main.c";
  d->func = NULL;

  g_list_store_append (store, d);

  while (demo->title)
    {
       DemoData *children = demo->children;

      if (demo_can_run (window, demo->name))
        {
          d = GTK_DEMO (g_object_new (GTK_TYPE_DEMO, NULL));

          d->name = demo->name;
          d->title = demo->title;
          d->keywords = demo->keywords;
          d->filename = demo->filename;
          d->func = demo->func;

          g_list_store_append (store, d);
        }

      if (children)
        {
          d->children_model = G_LIST_MODEL (g_list_store_new (GTK_TYPE_DEMO));

          while (children->title)
            {
              if (demo_can_run (window, children->name))
                {
                  GtkDemo *child = GTK_DEMO (g_object_new (GTK_TYPE_DEMO, NULL));

                  child->name = children->name;
                  child->title = children->title;
                  child->keywords = children->keywords;
                  child->filename = children->filename;
                  child->func = children->func;

                  g_list_store_append (G_LIST_STORE (d->children_model), child);
                }

              children++;
            }
        }

      demo++;
    }

  return G_LIST_MODEL (store);
}

static GListModel *
get_child_model (gpointer item,
                 gpointer user_data)
{
  GtkDemo *demo = item;

  if (demo->children_model)
    return g_object_ref (G_LIST_MODEL (demo->children_model));

  return NULL;
}

static void
clear_search (GtkSearchBar *bar)
{
  if (!gtk_search_bar_get_search_mode (bar))
    {
      GtkWidget *entry = gtk_search_bar_get_child (GTK_SEARCH_BAR (bar));
      gtk_editable_set_text (GTK_EDITABLE (entry), "");
    }
}

static void
search_results_update (GObject    *filter_model,
                       GParamSpec *pspec,
                       GtkEntry   *entry)
{
  gsize n_items = g_list_model_get_n_items (G_LIST_MODEL (filter_model));

  if (strlen (gtk_editable_get_text (GTK_EDITABLE (entry))) > 0)
    {
      char *text;

      if (n_items > 0)
        text = g_strdup_printf (ngettext ("%ld search result", "%ld search results", (long) n_items), (long) n_items);
      else
        text = g_strdup (_("No search results"));

      gtk_accessible_update_property (GTK_ACCESSIBLE (entry),
                                      GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, text,
                                      -1);

      g_free (text);
    }
  else
    {
      gtk_accessible_reset_property (GTK_ACCESSIBLE (entry), GTK_ACCESSIBLE_PROPERTY_DESCRIPTION);
    }
}

static gboolean
save_state_cb (GtkApplicationWindow *window,
               GVariantDict         *state)
{
  guint selected;
  int current;

  g_variant_dict_insert_value (state, "search-bar-visible", g_variant_new_boolean (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (search_bar))));

  g_variant_dict_insert_value (state, "search-string", g_variant_new_string (gtk_editable_get_text (GTK_EDITABLE (search_entry))));

  selected = gtk_single_selection_get_selected (selection);
  g_variant_dict_insert_value (state, "selected-item", g_variant_new_uint32 (selected));

  current = gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook));
  g_variant_dict_insert_value (state, "current-page", g_variant_new_int32 (current));

  return TRUE;
}

static void
select_item (gpointer data)
{
  gtk_single_selection_set_selected (selection, GPOINTER_TO_UINT (data));
}

static void
select_page (gpointer data)
{
  gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook), GPOINTER_TO_INT (data));
}

static void
restore_state (GtkApplicationWindow *window,
               GtkRestoreReason      reason,
               GVariant             *state)
{
  gboolean visible;
  guint selected;
  const char *string;
  int current;

  if (!state)
    return;

  if (reason != GTK_RESTORE_REASON_RESTORE)
    return;

  g_variant_lookup (state, "search-bar-visible", "b", &visible);
  gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (search_bar), visible);

  g_variant_lookup (state, "search-string", "&s", &string);
  if (string)
    {
      gtk_editable_set_text (GTK_EDITABLE (search_entry), string);
      gtk_editable_set_position (GTK_EDITABLE (search_entry), -1);
    }

  /* Wait for the search string filtering to take effect before
   * applying the selected-item
   */
  g_variant_lookup (state, "selected-item", "u", &selected);
  g_timeout_add_once (160, select_item, GUINT_TO_POINTER (selected));

  /* Wait for the selected-item to update the notebook before
   * applying the current-page
   */
  g_variant_lookup (state, "current-page", "i", &current);
  g_timeout_add_once (320, select_page, GINT_TO_POINTER (current));
}

static GtkWindow *
create_window (GtkApplication *app)
{
  GtkBuilder *builder;
  GListModel *listmodel;
  GtkTreeListModel *treemodel;
  GtkWidget *window, *listview;
  GtkFilterListModel *filter_model;
  GtkFilter *filter;
  GSimpleAction *action;

  builder = gtk_builder_new_from_resource ("/ui/main.ui");

  window = (GtkWidget *) gtk_builder_get_object (builder, "window");

  g_signal_connect (window, "save-state", G_CALLBACK (save_state_cb), NULL);

  gtk_application_add_window (app, GTK_WINDOW (window));

  if (g_strcmp0 (PROFILE, "devel") == 0)
    gtk_widget_add_css_class (window, "devel");

  action = g_simple_action_new ("run", NULL);
  g_signal_connect (action, "activate", G_CALLBACK (activate_run), window);
  g_action_map_add_action (G_ACTION_MAP (window), G_ACTION (action));

  g_clear_pointer (&current_file, g_free);
  notebook = GTK_WIDGET (gtk_builder_get_object (builder, "notebook"));

  info_view = GTK_WIDGET (gtk_builder_get_object (builder, "info-textview"));
  source_view = GTK_WIDGET (gtk_builder_get_object (builder, "source-textview"));
  toplevel = GTK_WIDGET (window);
  listview = GTK_WIDGET (gtk_builder_get_object (builder, "listview"));
  g_signal_connect (listview, "activate", G_CALLBACK (activate_cb), window);
  search_bar = GTK_WIDGET (gtk_builder_get_object (builder, "searchbar"));
  g_signal_connect (search_bar, "notify::search-mode-enabled", G_CALLBACK (clear_search), NULL);

  listmodel = create_demo_model (window);
  treemodel = gtk_tree_list_model_new (G_LIST_MODEL (listmodel),
                                       FALSE,
                                       TRUE,
                                       get_child_model,
                                       NULL,
                                       NULL);
  filter_model = gtk_filter_list_model_new (G_LIST_MODEL (treemodel), NULL);
  filter = GTK_FILTER (gtk_custom_filter_new (demo_filter_by_name, filter_model, NULL));
  gtk_filter_list_model_set_filter (filter_model, filter);
  g_object_unref (filter);

  search_entry = GTK_WIDGET (gtk_builder_get_object (builder, "search-entry"));
  g_signal_connect (search_entry, "search-changed", G_CALLBACK (demo_search_changed_cb), filter);
  g_signal_connect (filter_model, "notify::n-items", G_CALLBACK (search_results_update), search_entry);

  selection = gtk_single_selection_new (G_LIST_MODEL (filter_model));
  g_signal_connect (selection, "notify::selected-item", G_CALLBACK (selection_cb), NULL);
  gtk_list_view_set_model (GTK_LIST_VIEW (listview), GTK_SELECTION_MODEL (selection));

  selection_cb (selection, NULL, NULL);
  g_object_unref (selection);

  g_object_unref (builder);

  return GTK_WINDOW (window);
}

static void
restore_window (GtkApplication   *app,
                GtkRestoreReason  reason,
                GVariant         *state)
{
  GtkWindow *window;

  window = create_window (app);
  restore_state (GTK_APPLICATION_WINDOW (window), reason, state);
}

static void
activate (GApplication *gapp)
{
  GtkApplication *app = GTK_APPLICATION (gapp);
  GList *list;
  GtkWindow *window;

  list = gtk_application_get_windows (app);
  if (list)
    window = list->data;
  else
    window = create_window (app);

  gtk_window_present (window);
}

static void
auto_quit (gpointer data)
{
  g_application_quit (G_APPLICATION (data));
}

static void
list_demos (void)
{
  DemoData *d, *c;

  d = gtk_demos;

  while (d->title)
    {
      c = d->children;
      if (d->name)
        g_print ("%s\n", d->name);
      d++;
      while (c && c->title)
        {
          if (c->name)
            g_print ("%s\n", c->name);
          c++;
        }
    }
}

static int
command_line (GApplication            *app,
              GApplicationCommandLine *cmdline)
{
  GVariantDict *options;
  const char *name = NULL;
  gboolean autoquit = FALSE;
  gboolean list = FALSE;
  DemoData *d, *c;
  GDoDemoFunc func = 0;
  GtkWidget *window, *demo;

  options = g_application_command_line_get_options_dict (cmdline);
  g_variant_dict_lookup (options, "run", "&s", &name);
  g_variant_dict_lookup (options, "autoquit", "b", &autoquit);
  g_variant_dict_lookup (options, "list", "b", &list);

  if (name || autoquit || list)
    g_object_set (app, "support-save", FALSE, NULL);
    
  if (autoquit)
    g_timeout_add_seconds_once (1, auto_quit, app);

  if (!name && !list)
    {
      g_application_activate (app);
      return 0;
    }

  create_window (GTK_APPLICATION (app));

  if (list)
    {
      list_demos ();
      g_application_quit (app);
      return 0;
    }

  window = gtk_application_get_windows (GTK_APPLICATION (app))->data;

  if (name == NULL)
    goto out;

  d = gtk_demos;

  while (d->title)
    {
      c = d->children;
      if (g_strcmp0 (d->name, name) == 0)
        {
          func = d->func;
          goto out;
        }
      d++;
      while (c && c->title)
        {
          if (g_strcmp0 (c->name, name) == 0)
            {
              func = c->func;
              goto out;
            }
          c++;
        }
    }

out:
  if (func)
    {
      demo = (func) (window);

      gtk_window_set_transient_for (GTK_WINDOW (demo), GTK_WINDOW (window));
      gtk_widget_set_visible (window, FALSE);

      g_signal_connect_swapped (G_OBJECT (demo), "destroy", G_CALLBACK (g_application_quit), app);
    }

  return 0;
}

G_MODULE_EXPORT
int
main (int argc, char **argv)
{
  GtkApplication *app;
  static GActionEntry app_entries[] = {
    { "about", activate_about, NULL, NULL, NULL },
    { "quit", activate_quit, NULL, NULL, NULL },
    { "inspector", activate_inspector, NULL, NULL, NULL },
  };
  struct {
    const char *action_and_target;
    const char *accelerators[2];
  } accels[] = {
    { "app.about", { "F1", NULL } },
    { "app.quit", { "<Control>q", NULL } },
  };
  int i;
  char version[80];

  gtk_init ();

  app = gtk_application_new ("org.gtk.Demo4", G_APPLICATION_NON_UNIQUE|G_APPLICATION_HANDLES_COMMAND_LINE);
  g_object_set (app, "support-save", TRUE, NULL);

  g_snprintf (version, sizeof (version), "%s%s%s\n",
              PACKAGE_VERSION,
              g_strcmp0 (PROFILE, "devel") == 0 ? "-" : "",
              g_strcmp0 (PROFILE, "devel") == 0 ? VCS_TAG : "");

  g_application_set_version (G_APPLICATION (app), version);

  g_action_map_add_action_entries (G_ACTION_MAP (app),
                                   app_entries, G_N_ELEMENTS (app_entries),
                                   app);

  for (i = 0; i < G_N_ELEMENTS (accels); i++)
    gtk_application_set_accels_for_action (app, accels[i].action_and_target, accels[i].accelerators);

  g_application_add_main_option (G_APPLICATION (app), "run", 0, 0, G_OPTION_ARG_STRING, "Run an example", "EXAMPLE");
  g_application_add_main_option (G_APPLICATION (app), "list", 0, 0, G_OPTION_ARG_NONE, "List examples", NULL);
  g_application_add_main_option (G_APPLICATION (app), "autoquit", 0, 0, G_OPTION_ARG_NONE, "Quit after a delay", NULL);

  g_signal_connect (app, "command-line", G_CALLBACK (command_line), NULL);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  g_signal_connect (app, "restore-window", G_CALLBACK (restore_window), NULL);

  g_application_run (G_APPLICATION (app), argc, argv);

  return 0;
}
