[gnome-builder/wip/gtk4-port: 62/343] libide/gui: start on new preferences window




commit 2a5cb9854e2975a443e27e147a6e7d8aa5beee7a
Author: Christian Hergert <chergert redhat com>
Date:   Fri Mar 25 16:46:19 2022 -0700

    libide/gui: start on new preferences window
    
    The goal for this long term is to have a preferences window which can
    look more like the Control Center in terms of how it works but support
    both application preferences as well as project properties. They will
    be displayed at separate times, through separate flows however. Some
    settings which can have project overrides will be displayed redundantly
    in the project properties window.

 src/libide/gui/ide-preferences-window.c  | 720 ++++++++++++++++++++++++++++++-
 src/libide/gui/ide-preferences-window.h  |  90 +++-
 src/libide/gui/ide-preferences-window.ui | 100 ++++-
 src/libide/gui/meson.build               |   2 +
 src/libide/gui/tests/meson.build         |   4 +
 src/libide/gui/tests/test-preferences.c  | 314 ++++++++++++++
 6 files changed, 1214 insertions(+), 16 deletions(-)
---
diff --git a/src/libide/gui/ide-preferences-window.c b/src/libide/gui/ide-preferences-window.c
index 29a845bdc..3a25283a0 100644
--- a/src/libide/gui/ide-preferences-window.c
+++ b/src/libide/gui/ide-preferences-window.c
@@ -1,6 +1,6 @@
 /* ide-preferences-window.c
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2022 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
@@ -22,25 +22,739 @@
 
 #include "config.h"
 
+#include <glib/gi18n.h>
+
 #include "ide-preferences-window.h"
 
 struct _IdePreferencesWindow
 {
-  HdyApplicationWindow parent_window;
+  AdwApplicationWindow parent_window;
+
+  GtkToggleButton    *search_button;
+  GtkButton          *back_button;
+  GtkStack           *page_stack;
+  AdwWindowTitle     *page_title;
+  GtkStack           *pages_stack;
+  AdwWindowTitle     *pages_title;
+
+  const IdePreferencePageEntry *current_page;
+
+  guint rebuild_source;
+
+  struct {
+    GPtrArray *pages;
+    GPtrArray *groups;
+    GPtrArray *items;
+    GArray *data;
+  } info;
 };
 
-G_DEFINE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, HDY_TYPE_APPLICATION_WINDOW)
+typedef struct
+{
+  gpointer data;
+  GDestroyNotify notify;
+} DataDestroy;
+
+typedef struct
+{
+  GtkStack  *stack;
+  GtkWidget *child;
+} DropPage;
+
+typedef struct
+{
+  GtkBox *box;
+  GtkSearchBar *search_bar;
+  GtkSearchEntry *search_entry;
+  GtkScrolledWindow *scroller;
+  GtkListBox *list_box;
+} Page;
+
+G_DEFINE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, ADW_TYPE_APPLICATION_WINDOW)
+
+static gboolean
+drop_page_cb (gpointer data)
+{
+  DropPage *drop = data;
+  gtk_stack_remove (drop->stack, drop->child);
+  return G_SOURCE_REMOVE;
+}
+
+static DropPage *
+drop_page_new (GtkStack  *stack,
+               GtkWidget *child)
+{
+  DropPage *p = g_new0 (DropPage, 1);
+  p->stack = g_object_ref (stack);
+  p->child = g_object_ref (child);
+  return p;
+}
+
+static void
+drop_page_free (gpointer data)
+{
+  DropPage *drop = data;
+  g_object_unref (drop->child);
+  g_object_unref (drop->stack);
+  g_free (drop);
+}
+
+static gboolean
+entry_matches (const char *request,
+               const char *current)
+{
+  if (g_strcmp0 (request, current) == 0)
+    return TRUE;
+
+  if (g_str_has_suffix (request, "/*") &&
+      strncmp (request, current, strlen (request) - 2) == 0)
+    return TRUE;
+
+  return FALSE;
+}
+
+static const IdePreferencePageEntry *
+get_page (IdePreferencesWindow *self,
+          const char           *name)
+{
+  if (name == NULL)
+    return NULL;
+
+  for (guint i = 0; i < self->info.pages->len; i++)
+    {
+      const IdePreferencePageEntry *page = g_ptr_array_index (self->info.pages, i);
+
+      if (entry_matches (page->name, name))
+        return page;
+    }
+
+  return NULL;
+}
+
+static void
+go_back_cb (IdePreferencesWindow *self,
+            GtkButton            *button)
+{
+  const IdePreferencePageEntry *page;
+  const char *pages_name;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  pages_name = gtk_stack_get_visible_child_name (self->pages_stack);
+  page = get_page (self, pages_name);
+  if (page == NULL)
+    return;
+
+  if (!(page = get_page (self, page->parent)))
+    {
+      gtk_stack_set_visible_child_name (self->pages_stack, "default");
+
+      gtk_widget_hide (GTK_WIDGET (self->back_button));
+      gtk_widget_show (GTK_WIDGET (self->search_button));
+    }
+  else
+    {
+      gtk_stack_set_visible_child_name (self->pages_stack, page->name);
+    }
+}
+
+static gboolean
+filter_rows_cb (GtkListBoxRow *row,
+                gpointer       user_data)
+{
+  const IdePreferencePageEntry *page;
+  const char *text = user_data;
+
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (text != NULL);
+
+  page = g_object_get_data (G_OBJECT (row), "ENTRY");
+
+  /* TODO: Precalculate keywords/etc for pages */
+
+  if (strstr (page->name, text) != NULL ||
+      strstr (page->title, text) != NULL)
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+search_changed_cb (IdePreferencesWindow *self,
+                   GtkSearchEntry       *entry)
+{
+  const char *text;
+  GtkBox *box;
+  Page *page;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
+
+  box = GTK_BOX (gtk_widget_get_ancestor (GTK_WIDGET (entry), GTK_TYPE_BOX));
+  page = g_object_get_data (G_OBJECT (box), "PAGE");
+  g_assert (box == page->box);
+
+  text = gtk_editable_get_text (GTK_EDITABLE (entry));
+
+  if (text == NULL || text[0] == 0)
+    gtk_list_box_set_filter_func (page->list_box, NULL, NULL, NULL);
+  else
+    gtk_list_box_set_filter_func (page->list_box, filter_rows_cb, g_strdup (text), g_free);
+}
+
+static void
+ide_preferences_window_dispose (GObject *object)
+{
+  IdePreferencesWindow *self = (IdePreferencesWindow *)object;
+
+  g_clear_handle_id (&self->rebuild_source, g_source_remove);
+
+  if (self->info.data != NULL)
+    {
+      for (guint i = self->info.data->len; i > 0; i--)
+        {
+          DataDestroy data = g_array_index (self->info.data, DataDestroy, i-1);
+          g_array_remove_index (self->info.data, i-1);
+          data.notify (data.data);
+        }
+    }
+
+  g_clear_pointer (&self->info.pages, g_ptr_array_unref);
+  g_clear_pointer (&self->info.groups, g_ptr_array_unref);
+  g_clear_pointer (&self->info.items, g_ptr_array_unref);
+  g_clear_pointer (&self->info.data, g_array_unref);
+
+  G_OBJECT_CLASS (ide_preferences_window_parent_class)->dispose (object);
+}
 
 static void
 ide_preferences_window_class_init (IdePreferencesWindowClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_preferences_window_dispose;
 
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-preferences-window.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, page_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, pages_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, page_title);
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, pages_title);
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, search_button);
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesWindow, back_button);
+  gtk_widget_class_bind_template_callback (widget_class, go_back_cb);
 }
 
 static void
 ide_preferences_window_init (IdePreferencesWindow *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->info.pages = g_ptr_array_new_with_free_func (g_free);
+  self->info.groups = g_ptr_array_new_with_free_func (g_free);
+  self->info.items = g_ptr_array_new_with_free_func (g_free);
+  self->info.data = g_array_new (FALSE, FALSE, sizeof (DataDestroy));
+}
+
+GtkWidget *
+ide_preferences_window_new (void)
+{
+  return g_object_new (IDE_TYPE_PREFERENCES_WINDOW, NULL);
+}
+
+static int
+sort_pages_by_priority (gconstpointer a,
+                        gconstpointer b)
+{
+  const IdePreferencePageEntry * const *a_entry = a;
+  const IdePreferencePageEntry * const *b_entry = b;
+
+  if ((*a_entry)->priority < (*b_entry)->priority)
+    return -1;
+  else if ((*a_entry)->priority > (*b_entry)->priority)
+    return 1;
+  else
+    return 0;
+}
+
+static int
+sort_groups_by_priority (gconstpointer a,
+                         gconstpointer b)
+{
+  const IdePreferenceGroupEntry * const *a_entry = a;
+  const IdePreferenceGroupEntry * const *b_entry = b;
+
+  if ((*a_entry)->priority < (*b_entry)->priority)
+    return -1;
+  else if ((*a_entry)->priority > (*b_entry)->priority)
+    return 1;
+  else
+    return 0;
+}
+
+static int
+sort_items_by_priority (gconstpointer a,
+                        gconstpointer b)
+{
+  const IdePreferenceItemEntry * const *a_entry = a;
+  const IdePreferenceItemEntry * const *b_entry = b;
+
+  if ((*a_entry)->priority < (*b_entry)->priority)
+    return -1;
+  else if ((*a_entry)->priority > (*b_entry)->priority)
+    return 1;
+  else
+    return 0;
+}
+
+static gboolean
+has_children (GPtrArray  *pages,
+              const char *page)
+{
+  for (guint i = 0; i < pages->len; i++)
+    {
+      IdePreferencePageEntry *entry = g_ptr_array_index (pages, i);
+
+      if (g_strcmp0 (entry->parent, page) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+pages_header_func (GtkListBoxRow *row,
+                   GtkListBoxRow *before,
+                   gpointer       user_data)
+{
+  if (before != NULL &&
+      g_object_get_data (G_OBJECT (row), "SECTION") != g_object_get_data (G_OBJECT (before), "SECTION"))
+    gtk_list_box_row_set_header (row, gtk_separator_new (GTK_ORIENTATION_HORIZONTAL));
+  else
+    gtk_list_box_row_set_header (row, NULL);
+}
+
+static GtkListBoxRow *
+add_page (IdePreferencesWindow         *self,
+          GtkListBox                   *list_box,
+          GPtrArray                    *pages,
+          const IdePreferencePageEntry *entry)
+{
+  GtkListBoxRow *row;
+  GtkImage *icon;
+  GtkLabel *title;
+  GtkBox *box;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (entry != NULL);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      NULL);
+  box = g_object_new (GTK_TYPE_BOX,
+                      "spacing", 12,
+                      "margin-top", 12,
+                      "margin-bottom", 12,
+                      "margin-start", 12,
+                      "margin-end", 12,
+                      NULL);
+  icon = g_object_new (GTK_TYPE_IMAGE,
+                       "icon-name", entry->icon_name,
+                       NULL);
+  title = g_object_new (GTK_TYPE_LABEL,
+                        "label", entry->title,
+                        "xalign", 0.0f,
+                        "hexpand", TRUE,
+                        NULL);
+  gtk_box_append (box, GTK_WIDGET (icon));
+  gtk_box_append (box, GTK_WIDGET (title));
+  gtk_list_box_row_set_child (row, GTK_WIDGET (box));
+
+  if (has_children (pages, entry->name))
+    {
+      GtkImage *more;
+
+      more = g_object_new (GTK_TYPE_IMAGE,
+                           "icon-name", "go-next-symbolic",
+                           NULL);
+      gtk_box_append (box, GTK_WIDGET (more));
+    }
+
+  g_object_set_data (G_OBJECT (row), "ENTRY", (gpointer)entry);
+  g_object_set_data (G_OBJECT (row), "SECTION", (gpointer)entry->section);
+
+  gtk_list_box_append (list_box, GTK_WIDGET (row));
+
+  return row;
+}
+
+static void
+ide_preferences_window_page_activated_cb (IdePreferencesWindow *self,
+                                          GtkListBoxRow        *row,
+                                          GtkListBox           *list_box)
+{
+  const IdePreferencePageEntry *entry;
+  const IdePreferencePageEntry *parent;
+  AdwPreferencesPage *page;
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  entry = g_object_get_data (G_OBJECT (row), "ENTRY");
+  if (entry == self->current_page)
+    return;
+
+  self->current_page = entry;
+  parent = get_page (self, entry->parent);
+
+  visible_child = gtk_stack_get_visible_child (self->page_stack);
+
+  adw_window_title_set_title (self->page_title, entry->title);
+
+  if (parent != NULL)
+    adw_window_title_set_title (self->pages_title, parent->title);
+  else
+    adw_window_title_set_title (self->pages_title, _("Preferences"));
+
+  if (has_children (self->info.pages, entry->name))
+    {
+      GtkListBoxRow *subrow;
+      Page *info;
+
+      gtk_stack_set_visible_child_name (self->pages_stack, entry->name);
+
+      info = g_object_get_data (G_OBJECT (gtk_stack_get_visible_child (self->pages_stack)), "PAGE");
+      subrow = gtk_list_box_get_row_at_index (info->list_box, 0);
+
+      gtk_widget_hide (GTK_WIDGET (self->search_button));
+      gtk_widget_show (GTK_WIDGET (self->back_button));
+
+      /* Now select the first row of the child and bail out as we'll reenter
+       * this function with that row selected.
+       */
+      gtk_list_box_select_row (info->list_box, subrow);
+
+      return;
+    }
+  else if (entry->parent == NULL)
+    {
+      gtk_stack_set_visible_child_name (self->pages_stack, "default");
+    }
+
+  /* First create the new page so we can transition to it. Then
+   * remove the old page from a callback after the transition has
+   * completed.
+   */
+  page = ADW_PREFERENCES_PAGE (adw_preferences_page_new ());
+  adw_preferences_page_set_title (page, entry->title);
+  adw_preferences_page_set_name (page, entry->name);
+
+  /* XXX: this could be optimized with sort/binary search */
+  for (guint i = 0; i < self->info.groups->len; i++)
+    {
+      const IdePreferenceGroupEntry *group = g_ptr_array_index (self->info.groups, i);
+
+      if (entry_matches (group->page, entry->name))
+        {
+          AdwPreferencesGroup *pref_group;
+
+          pref_group = ADW_PREFERENCES_GROUP (adw_preferences_group_new ());
+          adw_preferences_group_set_title (pref_group, group->title);
+
+          /* XXX: this could be optimized with sort/binary search */
+          for (guint j = 0; j < self->info.items->len; j++)
+            {
+              const IdePreferenceItemEntry *item = g_ptr_array_index (self->info.items, j);
+
+              if (entry_matches (item->page, entry->name) &&
+                  entry_matches (item->group, group->name))
+                item->callback (entry->name, item, pref_group, (gpointer)item->reserved1);
+            }
+
+          adw_preferences_page_add (page, pref_group);
+        }
+    }
+
+  /* Now add the new child and transition to it */
+  gtk_stack_add_child (self->page_stack, GTK_WIDGET (page));
+  gtk_stack_set_visible_child (self->page_stack, GTK_WIDGET (page));
+
+  if (visible_child != NULL)
+    g_timeout_add_full (G_PRIORITY_LOW,
+                        gtk_stack_get_transition_duration (self->page_stack) + 100,
+                        drop_page_cb,
+                        drop_page_new (self->page_stack, visible_child),
+                        drop_page_free);
+}
+
+static void
+create_navigation_page (IdePreferencesWindow  *self,
+                        Page                 **out_page)
+{
+  Page *page;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (out_page != NULL);
+
+  page = g_new0 (Page, 1);
+  page->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            NULL);
+  page->search_entry = g_object_new (GTK_TYPE_SEARCH_ENTRY,
+                                     "hexpand", TRUE,
+                                     NULL);
+  g_signal_connect_object (page->search_entry,
+                           "changed",
+                           G_CALLBACK (search_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  page->search_bar = g_object_new (GTK_TYPE_SEARCH_BAR,
+                                   "child", page->search_entry,
+                                   NULL);
+  page->scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                                 "hscrollbar-policy", GTK_POLICY_NEVER,
+                                 "vexpand", TRUE,
+                                 NULL);
+  page->list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                                 "activate-on-single-click", TRUE,
+                                 "selection-mode", GTK_SELECTION_SINGLE,
+                                 NULL);
+  g_signal_connect_object (page->list_box,
+                           "row-activated",
+                           G_CALLBACK (ide_preferences_window_page_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_list_box_set_header_func (page->list_box, pages_header_func, NULL, NULL);
+  gtk_widget_add_css_class (GTK_WIDGET (page->list_box), "navigation-sidebar");
+  gtk_box_append (page->box, GTK_WIDGET (page->search_bar));
+  gtk_box_append (page->box, GTK_WIDGET (page->scroller));
+  gtk_scrolled_window_set_child (page->scroller, GTK_WIDGET (page->list_box));
+
+  g_object_set_data_full (G_OBJECT (page->box), "PAGE", page, g_free);
+
+  *out_page = page;
+}
+
+static void
+ide_preferences_window_rebuild (IdePreferencesWindow *self)
+{
+  GtkListBoxRow *select_row = NULL;
+  GHashTable *pages;
+  Page *page;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+
+  /* Remove old widgets */
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self->pages_stack));
+       child != NULL;
+       child = gtk_widget_get_first_child (GTK_WIDGET (self->pages_stack)))
+    gtk_stack_remove (self->pages_stack, child);
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self->page_stack));
+       child != NULL;
+       child = gtk_widget_get_first_child (GTK_WIDGET (self->page_stack)))
+    gtk_stack_remove (self->page_stack, child);
+
+  /* Clear titles */
+  adw_window_title_set_title (self->page_title, NULL);
+  adw_window_title_set_title (self->pages_title, _("Preferences"));
+
+  if (self->info.pages->len == 0)
+    return;
+
+  pages = g_hash_table_new (NULL, NULL);
+
+  /* Add new pages */
+  g_ptr_array_sort (self->info.pages, sort_pages_by_priority);
+  g_ptr_array_sort (self->info.groups, sort_groups_by_priority);
+  g_ptr_array_sort (self->info.items, sort_items_by_priority);
+
+  create_navigation_page (self, &page);
+  g_object_bind_property (self->search_button, "active",
+                          page->search_bar, "search-mode-enabled",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+  gtk_stack_add_named (self->pages_stack, GTK_WIDGET (page->box), "default");
+  gtk_stack_set_visible_child (self->pages_stack, GTK_WIDGET (page->box));
+
+  for (guint i = 0; i < self->info.pages->len; i++)
+    {
+      const IdePreferencePageEntry *entry = g_ptr_array_index (self->info.pages, i);
+      GtkListBox *parent = page->list_box;
+      GtkListBoxRow *row;
+
+      if (entry->parent != NULL)
+        {
+          parent = g_hash_table_lookup (pages, entry->parent);
+
+          if (parent == NULL)
+            {
+              Page *subpage;
+              create_navigation_page (self, &subpage);
+              gtk_search_bar_set_search_mode (subpage->search_bar, TRUE);
+              gtk_stack_add_named (self->pages_stack, GTK_WIDGET (subpage->box), entry->parent);
+              parent = subpage->list_box;
+              g_hash_table_insert (pages, (gpointer)entry->parent, parent);
+            }
+        }
+
+      row = add_page (self, parent, self->info.pages, entry);
+      if (select_row == NULL)
+        select_row = row;
+    }
+
+  /* Now select the first row */
+  gtk_widget_activate (GTK_WIDGET (select_row));
+
+  g_hash_table_unref (pages);
+}
+
+static gboolean
+ide_preferences_window_rebuild_cb (gpointer data)
+{
+  IdePreferencesWindow *self = data;
+  self->rebuild_source = 0;
+  ide_preferences_window_rebuild (self);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_preferences_window_queue_rebuild (IdePreferencesWindow *self)
+{
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (self->rebuild_source == 0)
+    self->rebuild_source = g_idle_add (ide_preferences_window_rebuild_cb, self);
+}
+
+void
+ide_preferences_window_add_pages (IdePreferencesWindow         *self,
+                                  const IdePreferencePageEntry *pages,
+                                  gsize                         n_pages,
+                                  const char                   *translation_domain)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (pages != NULL || n_pages == 0);
+
+  for (gsize i = 0; i < n_pages; i++)
+    {
+      IdePreferencePageEntry entry = pages[i];
+
+      entry.parent = g_intern_string (entry.parent);
+      entry.section = g_intern_string (entry.section);
+      entry.name = g_intern_string (entry.name);
+      entry.icon_name = g_intern_string (entry.icon_name);
+      entry.title = g_intern_string (g_dgettext (translation_domain, entry.title));
+
+      g_ptr_array_add (self->info.pages, g_memdup2 (&entry, sizeof entry));
+    }
+
+  ide_preferences_window_queue_rebuild (self);
+}
+
+void
+ide_preferences_window_add_groups (IdePreferencesWindow          *self,
+                                   const IdePreferenceGroupEntry *groups,
+                                   gsize                          n_groups,
+                                   const char                    *translation_domain)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (groups != NULL || n_groups == 0);
+
+  for (gsize i = 0; i < n_groups; i++)
+    {
+      IdePreferenceGroupEntry entry = groups[i];
+
+      entry.page = g_intern_string (entry.page);
+      entry.name = g_intern_string (entry.name);
+      entry.title = g_intern_string (g_dgettext (translation_domain, entry.title));
+
+      g_ptr_array_add (self->info.groups, g_memdup2 (&entry, sizeof entry));
+    }
+
+  ide_preferences_window_queue_rebuild (self);
+}
+
+static gboolean noop (gpointer data) { return G_SOURCE_REMOVE; };
+
+void
+ide_preferences_window_add_items (IdePreferencesWindow         *self,
+                                  const IdePreferenceItemEntry *items,
+                                  gsize                         n_items,
+                                  gpointer                      user_data,
+                                  GDestroyNotify                user_data_destroy)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (items != NULL || n_items == 0);
+
+  if (n_items == 0)
+    {
+      if (user_data_destroy)
+        g_idle_add_full (G_PRIORITY_DEFAULT, noop, user_data, user_data_destroy);
+      return;
+    }
+
+  for (gsize i = 0; i < n_items; i++)
+    {
+      IdePreferenceItemEntry entry = items[i];
+
+      if (entry.callback == NULL)
+        continue;
+
+      entry.page = g_intern_string (entry.page);
+      entry.group = g_intern_string (entry.group);
+      entry.name = g_intern_string (entry.name);
+      entry.reserved1 = user_data;
+
+      g_ptr_array_add (self->info.items, g_memdup2 (&entry, sizeof entry));
+    }
+
+  if (user_data_destroy)
+    {
+      DataDestroy data;
+
+      data.data = user_data;
+      data.notify = user_data_destroy;
+
+      g_array_append_val (self->info.data, data);
+    }
+
+  ide_preferences_window_queue_rebuild (self);
+}
+
+void
+ide_preferences_window_add_item (IdePreferencesWindow  *self,
+                                 const char            *page,
+                                 const char            *group,
+                                 const char            *name,
+                                 int                    priority,
+                                 IdePreferenceCallback  callback,
+                                 gpointer               user_data,
+                                 GDestroyNotify         user_data_destroy)
+{
+  IdePreferenceItemEntry entry;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (page != NULL);
+  g_return_if_fail (group != NULL);
+  g_return_if_fail (callback != NULL);
+
+  entry.page = g_intern_string (page);
+  entry.group = g_intern_string (group);
+  entry.name = g_intern_string (name);
+  entry.reserved1 = user_data;
+
+  g_ptr_array_add (self->info.items, g_memdup2 (&entry, sizeof entry));
+
+  if (user_data_destroy)
+    {
+      DataDestroy data;
+
+      data.data = user_data;
+      data.notify = user_data_destroy;
+
+      g_array_append_val (self->info.data, data);
+    }
+
+  ide_preferences_window_queue_rebuild (self);
 }
diff --git a/src/libide/gui/ide-preferences-window.h b/src/libide/gui/ide-preferences-window.h
index 9b7d374a0..dec48c421 100644
--- a/src/libide/gui/ide-preferences-window.h
+++ b/src/libide/gui/ide-preferences-window.h
@@ -1,6 +1,6 @@
 /* ide-preferences-window.h
  *
- * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2022 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
@@ -20,15 +20,95 @@
 
 #pragma once
 
-#include <dazzle.h>
-#include <handy.h>
+#include <adwaita.h>
+
 #include <libide-core.h>
 
 G_BEGIN_DECLS
 
+typedef struct _IdePreferenceItemEntry IdePreferenceItemEntry;
+
+typedef void (*IdePreferenceCallback) (const char                   *page_name,
+                                       const IdePreferenceItemEntry *entry,
+                                       AdwPreferencesGroup          *group,
+                                       gpointer                      user_data);
+
+typedef struct
+{
+  const char *parent;
+  const char *section;
+  const char *name;
+  const char *icon_name;
+  int priority;
+  const char *title;
+
+  /*< private >*/
+  gconstpointer reserved1;
+  gconstpointer reserved2;
+  gconstpointer reserved3;
+  gconstpointer reserved4;
+} IdePreferencePageEntry;
+
+typedef struct
+{
+  const char *page;
+  const char *name;
+  int priority;
+  const char *title;
+
+  /*< private >*/
+  gconstpointer reserved1;
+  gconstpointer reserved2;
+  gconstpointer reserved3;
+  gconstpointer reserved4;
+} IdePreferenceGroupEntry;
+
+struct _IdePreferenceItemEntry
+{
+  const char *page;
+  const char *group;
+  const char *name;
+  int priority;
+  IdePreferenceCallback callback;
+
+  /*< private >*/
+  gconstpointer reserved1;
+  gconstpointer reserved2;
+  gconstpointer reserved3;
+  gconstpointer reserved4;
+};
+
 #define IDE_TYPE_PREFERENCES_WINDOW (ide_preferences_window_get_type())
 
-IDE_AVAILABLE_IN_3_32
-G_DECLARE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, IDE, PREFERENCES_WINDOW, 
HdyApplicationWindow)
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, IDE, PREFERENCES_WINDOW, 
AdwApplicationWindow)
+
+IDE_AVAILABLE_IN_ALL
+GtkWidget *ide_preferences_window_new        (void);
+IDE_AVAILABLE_IN_ALL
+void       ide_preferences_window_add_pages  (IdePreferencesWindow         *self,
+                                              const IdePreferencePageEntry *pages,
+                                              gsize                         n_pages,
+                                              const char                   *translation_domain);
+IDE_AVAILABLE_IN_ALL
+void       ide_preferences_window_add_groups (IdePreferencesWindow          *self,
+                                              const IdePreferenceGroupEntry *groups,
+                                              gsize                          n_groups,
+                                              const char                   *translation_domain);
+IDE_AVAILABLE_IN_ALL
+void       ide_preferences_window_add_items  (IdePreferencesWindow          *self,
+                                              const IdePreferenceItemEntry  *items,
+                                              gsize                          n_items,
+                                              gpointer                       user_data,
+                                              GDestroyNotify                 user_data_destroy);
+IDE_AVAILABLE_IN_ALL
+void       ide_preferences_window_add_item   (IdePreferencesWindow          *self,
+                                              const char                    *page,
+                                              const char                    *group,
+                                              const char                    *name,
+                                              int                            priority,
+                                              IdePreferenceCallback          callback,
+                                              gpointer                       user_data,
+                                              GDestroyNotify                 user_data_destroy);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.ui b/src/libide/gui/ide-preferences-window.ui
index 118a49e10..107b4644d 100644
--- a/src/libide/gui/ide-preferences-window.ui
+++ b/src/libide/gui/ide-preferences-window.ui
@@ -1,23 +1,107 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <template class="IdePreferencesWindow" parent="HdyApplicationWindow">
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="Adw" version="1.0"/>
+  <template class="IdePreferencesWindow" parent="AdwApplicationWindow">
     <child>
       <object class="GtkBox">
         <property name="orientation">vertical</property>
-        <property name="visible">true</property>
         <child>
-          <object class="HdyHeaderBar" id="header_bar">
-            <property name="title" translatable="yes">Preferences</property>
-            <property name="show-close-button">true</property>
-            <property name="visible">true</property>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <child>
+              <object class="AdwHeaderBar" id="left">
+                <property name="title-widget">
+                  <object class="AdwWindowTitle" id="pages_title">
+                  </object>
+                </property>
+                <property name="show-start-title-buttons">false</property>
+                <property name="show-end-title-buttons">false</property>
+                <property name="width-request">300</property>
+                <child type="start">
+                  <object class="GtkToggleButton" id="search_button">
+                    <property name="icon-name">edit-find-symbolic</property>
+                  </object>
+                </child>
+                <child type="start">
+                  <object class="GtkButton" id="back_button">
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="visible">false</property>
+                    <signal name="clicked" handler="go_back_cb" swapped="true" 
object="IdePreferencesWindow"/>
+                  </object>
+                </child>
+                <child type="end">
+                  <object class="GtkMenuButton" id="primary_button">
+                    <property name="icon-name">open-menu-symbolic</property>
+                    <property name="menu-model">primary_menu</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="orientation">vertical</property>
+              </object>
+            </child>
+            <child>
+              <object class="AdwHeaderBar" id="right">
+                <property name="hexpand">true</property>
+                <property name="show-end-title-buttons">true</property>
+                <property name="title-widget">
+                  <object class="AdwWindowTitle" id="page_title">
+                  </object>
+                </property>
+              </object>
+            </child>
           </object>
         </child>
         <child>
-          <object class="IdePreferencesSurface" id="surface">
-            <property name="visible">true</property>
+          <object class="GtkBox">
+            <property name="vexpand">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="width-request">300</property>
+                <property name="hexpand">false</property>
+                <child>
+                  <object class="GtkStack" id="pages_stack">
+                    <property name="transition-type">slide-left</property>
+                    <property name="transition-duration">300</property>
+                    <property name="vexpand">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSeparator">
+                <property name="orientation">vertical</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkStack" id="page_stack">
+                <property name="hexpand">true</property>
+                <property name="vexpand">true</property>
+                <property name="transition-type">crossfade</property>
+                <property name="transition-duration">300</property>
+              </object>
+            </child>
           </object>
         </child>
       </object>
     </child>
   </template>
+  <menu id="primary_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">win.show-help-overlay</attribute>
+        <attribute name="accel">&lt;ctrl&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">win.show-help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
index eb38c7ea8..22aa96778 100644
--- a/src/libide/gui/meson.build
+++ b/src/libide/gui/meson.build
@@ -199,3 +199,5 @@ gnome_builder_private_sources += files(libide_gui_private_sources)
 gnome_builder_private_headers += files(libide_gui_private_headers)
 gnome_builder_include_subdirs += libide_gui_header_subdir
 gnome_builder_gir_extra_args += ['--c-include=libide-gui.h', '-DIDE_GUI_COMPILATION']
+
+subdir('tests')
diff --git a/src/libide/gui/tests/meson.build b/src/libide/gui/tests/meson.build
new file mode 100644
index 000000000..0e82e37e7
--- /dev/null
+++ b/src/libide/gui/tests/meson.build
@@ -0,0 +1,4 @@
+test_preferences = executable('test-preferences',
+  [ 'test-preferences.c', '../ide-preferences-window.c', libide_gui_resources ],
+  dependencies: [libadwaita_dep, libide_core_dep, libgtksource_dep],
+)
diff --git a/src/libide/gui/tests/test-preferences.c b/src/libide/gui/tests/test-preferences.c
new file mode 100644
index 000000000..7bba01f8c
--- /dev/null
+++ b/src/libide/gui/tests/test-preferences.c
@@ -0,0 +1,314 @@
+#include "../ide-preferences-window.h"
+
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+
+static void create_source_view_cb (const char                   *page,
+                                   const IdePreferenceItemEntry *item,
+                                   AdwPreferencesGroup          *pref_group,
+                                   gpointer                      user_data);
+static void create_schemes_cb (const char                   *page,
+                               const IdePreferenceItemEntry *item,
+                               AdwPreferencesGroup          *pref_group,
+                               gpointer                      user_data);
+static void select_font_cb (const char                   *page,
+                            const IdePreferenceItemEntry *item,
+                            AdwPreferencesGroup          *pref_group,
+                            gpointer                      user_data);
+static void create_style_cb (const char                   *page,
+                             const IdePreferenceItemEntry *item,
+                             AdwPreferencesGroup          *pref_group,
+                             gpointer                      user_data);
+static void toggle_cb (const char                   *page,
+                       const IdePreferenceItemEntry *item,
+                       AdwPreferencesGroup          *pref_group,
+                       gpointer                      user_data);
+static void spin_cb (const char                   *page,
+                     const IdePreferenceItemEntry *item,
+                     AdwPreferencesGroup          *pref_group,
+                     gpointer                      user_data);
+
+static const IdePreferencePageEntry pages[] = {
+  { NULL, "visual", "appearance", "org.gnome.Builder-appearance-symbolic", 0, "Appearance" },
+  { NULL, "visual", "editing", "org.gnome.Builder-editing-symbolic", 10, "Editing" },
+  { NULL, "visual", "keyboard", "org.gnome.Builder-shortcuts-symbolic", 20, "Shortcuts" },
+  { NULL, "code", "languages", "org.gnome.Builder-languages-symbolic", 100, "Languages" },
+  { NULL, "code", "completion", "org.gnome.Builder-completion-symbolic", 110, "Completion" },
+  { NULL, "code", "insight", "org.gnome.Builder-diagnostics-symbolic", 120, "Diagnostics" },
+  { NULL, "projects", "projects", "org.gnome.Builder-projects-symbolic", 200, "Projects" },
+  { NULL, "tools", "build", "org.gnome.Builder-build-symbolic", 300, "Build" },
+  { NULL, "tools", "debug", "org.gnome.Builder-debugger-symbolic", 310, "Debugger" },
+  { NULL, "tools", "commands", "org.gnome.Builder-command-symbolic", 320, "Commands" },
+  { NULL, "tools", "sdks", "org.gnome.Builder-sdk-symbolic", 500, "SDKs" },
+  { NULL, "plugins", "plugins", "org.gnome.Builder-plugins-symbolic", 600, "Plugins" },
+};
+
+static const IdePreferenceGroupEntry groups[] = {
+  { "appearance", "style", 0, "Appearance" },
+  { "appearance", "preview", 0, "Style" },
+  { "appearance", "schemes", 10, NULL },
+  { "appearance", "font", 20, NULL },
+  { "appearance", "accessories", 20, NULL },
+
+  { "languages/*", "general", 0, "General" },
+  { "languages/*", "margins", 10, "Margins" },
+  { "languages/*", "spacing", 20, "Spacing" },
+  { "languages/*", "indentation", 30, "Indentation" },
+};
+
+static const IdePreferenceItemEntry items[] = {
+  { "appearance", "style", "style", 0, create_style_cb },
+  { "appearance", "preview", "sourceview", 0, create_source_view_cb },
+  { "appearance", "schemes", "schemes", 0, create_schemes_cb },
+  { "appearance", "font", "font", 0, select_font_cb },
+};
+
+static const IdePreferenceItemEntry lang_items[] = {
+  { "languages/*", "general", "trim", 0, toggle_cb, NULL, "Trim Trailing Whitespace", "Upon saving, trailing 
whitepsace from modified lines will be trimmed." },
+  { "languages/*", "general", "overwrite", 0, toggle_cb, NULL, "Overwrite Braces", "Overwrite closing 
braces" },
+  { "languages/*", "general", "insert-matching", 0, toggle_cb, NULL, "Insert Matching Brace", "Insert 
matching character for [[(\"'" },
+  { "languages/*", "general", "insert-trailing", 0, toggle_cb, NULL, "Insert Trailing Newline", "Ensure 
files end with a newline" },
+
+  { "languages/*", "margins", "show-right-margin", 0, toggle_cb, NULL, "Show right margin", "Display a 
margin in the editor to indicate maximum desired width" },
+  { "languages/*", "margins", "right-margin", 0, spin_cb, NULL, "Right margin position", "The position of 
the right margin in characters" },
+
+  { "languages/*", "spacing", "before-parens", 0, toggle_cb, NULL, "Prefer a space before opening 
parentheses" },
+  { "languages/*", "spacing", "before-brackets", 0, toggle_cb, NULL, "Prefer a space before opening 
brackets" },
+  { "languages/*", "spacing", "before-braces", 0, toggle_cb, NULL, "Prefer a space before opening braces" },
+  { "languages/*", "spacing", "before-angles", 0, toggle_cb, NULL, "Prefer a space before opening angles" },
+
+  { "languages/*", "indentation", "tab-width", 0, spin_cb, NULL, "Tab width", "Width of a tab character in 
spaces" },
+  { "languages/*", "indentation", "insert-spaces", 0, toggle_cb, NULL, "Insert spaces instead of tabs", 
"Prefer spaces over tabs" },
+  { "languages/*", "indentation", "auto-indent", 0, toggle_cb, NULL, "Automatically Indent", "Format source 
code as you type" },
+
+};
+
+static int
+compare_section (gconstpointer a,
+                 gconstpointer b)
+{
+  const IdePreferencePageEntry *pagea = a;
+  const IdePreferencePageEntry *pageb = b;
+
+  return g_strcmp0 (pagea->section, pageb->section);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+  IdePreferencesWindow *window;
+  GtkSourceLanguageManager *langs;
+  const char * const *lang_ids;
+  GMainLoop *main_loop;
+  IdePreferencePageEntry *lpages;
+  guint j = 0;
+
+  gtk_init ();
+  adw_init ();
+  gtk_source_init ();
+
+  main_loop = g_main_loop_new (NULL, FALSE);
+  window = IDE_PREFERENCES_WINDOW (ide_preferences_window_new ());
+
+  gtk_window_set_default_size (GTK_WINDOW (window), 1200, 900);
+
+  ide_preferences_window_add_pages (window, pages, G_N_ELEMENTS (pages), NULL);
+  ide_preferences_window_add_groups (window, groups, G_N_ELEMENTS (groups), NULL);
+  ide_preferences_window_add_items (window, items, G_N_ELEMENTS (items), NULL, NULL);
+  ide_preferences_window_add_items (window, lang_items, G_N_ELEMENTS (lang_items), NULL, NULL);
+
+  langs = gtk_source_language_manager_get_default ();
+  lang_ids = gtk_source_language_manager_get_language_ids (langs);
+  lpages = g_new0 (IdePreferencePageEntry, g_strv_length ((char **)lang_ids));
+
+  for (guint i = 0; lang_ids[i]; i++)
+    {
+      GtkSourceLanguage *l = gtk_source_language_manager_get_language (langs, lang_ids[i]);
+      IdePreferencePageEntry *page;
+      char name[256];
+
+      if (gtk_source_language_get_hidden (l))
+        continue;
+
+      page = &lpages[j++];
+
+      g_snprintf (name, sizeof name, "languages/%s", lang_ids[i]);
+
+      page->parent = "languages";
+      page->section = gtk_source_language_get_section (l);
+      page->name = g_intern_string (name);
+      page->icon_name = NULL;
+      page->title = gtk_source_language_get_name (l);
+    }
+
+  qsort (lpages, j, sizeof *lpages, compare_section);
+  for (guint i = 0; i < j; i++)
+    lpages[i].priority = i;
+  ide_preferences_window_add_pages (window, lpages, j, NULL);
+  g_free (lpages);
+
+  g_signal_connect_swapped (window, "close-request", G_CALLBACK (g_main_loop_quit), main_loop);
+  gtk_window_present (GTK_WINDOW (window));
+  g_main_loop_run (main_loop);
+
+  return 0;
+}
+
+static void
+create_source_view_cb (const char                   *page,
+                       const IdePreferenceItemEntry *item,
+                       AdwPreferencesGroup          *pref_group,
+                       gpointer                      user_data)
+{
+  GtkWidget *frame;
+  GtkWidget *view;
+
+  frame = g_object_new (GTK_TYPE_FRAME, NULL);
+  view = g_object_new (GTK_SOURCE_TYPE_VIEW,
+                       "show-line-numbers", TRUE,
+                       "highlight-current-line", TRUE,
+                       "hexpand", TRUE,
+                       NULL);
+  gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)), "\n\n\n\n", -1);
+  gtk_frame_set_child (GTK_FRAME (frame), view);
+  adw_preferences_group_add (pref_group, frame);
+}
+
+static void
+create_schemes_cb (const char                   *page,
+                   const IdePreferenceItemEntry *item,
+                   AdwPreferencesGroup          *pref_group,
+                   gpointer                      user_data)
+{
+  GtkSourceStyleSchemeManager *manager = gtk_source_style_scheme_manager_get_default ();
+  const char * const *scheme_ids;
+  GtkFlowBox *flow;
+
+  flow = g_object_new (GTK_TYPE_FLOW_BOX,
+                       "column-spacing", 6,
+                       "row-spacing", 6,
+                       NULL);
+
+  scheme_ids = gtk_source_style_scheme_manager_get_scheme_ids (manager);
+
+  for (guint i = 0; scheme_ids[i]; i++)
+    {
+      GtkSourceStyleScheme *scheme = gtk_source_style_scheme_manager_get_scheme (manager, scheme_ids[i]);
+      GtkWidget *preview = gtk_source_style_scheme_preview_new (scheme);
+
+      gtk_flow_box_append (flow, preview);
+    }
+
+  adw_preferences_group_add (pref_group, GTK_WIDGET (flow));
+}
+
+static void
+select_font_cb (const char                   *page,
+                const IdePreferenceItemEntry *item,
+                AdwPreferencesGroup          *pref_group,
+                gpointer                      user_data)
+{
+  AdwExpanderRow *row;
+  AdwActionRow *font;
+
+  row = g_object_new (ADW_TYPE_EXPANDER_ROW,
+                      "title", _("Custom Font"),
+                      "show-enable-switch", TRUE,
+                      NULL);
+  adw_preferences_group_add (pref_group, GTK_WIDGET (row));
+
+  font = g_object_new (ADW_TYPE_ACTION_ROW,
+                       "title", "Editor",
+                       "subtitle", "Monospace 11",
+                       NULL);
+  adw_action_row_add_suffix (font, gtk_image_new_from_icon_name ("go-next-symbolic"));
+  adw_expander_row_add_row (row, GTK_WIDGET (font));
+
+  font = g_object_new (ADW_TYPE_ACTION_ROW,
+                       "title", "Terminal",
+                       "subtitle", "Monospace 11",
+                       NULL);
+  adw_action_row_add_suffix (font, gtk_image_new_from_icon_name ("go-next-symbolic"));
+  adw_expander_row_add_row (row, GTK_WIDGET (font));
+}
+
+static void
+create_style_cb (const char                   *page,
+                 const IdePreferenceItemEntry *item,
+                 AdwPreferencesGroup          *pref_group,
+                 gpointer                      user_data)
+{
+  GtkBox *box;
+  AdwPreferencesRow *row;
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "margin-top", 24,
+                      "margin-end", 24,
+                      "margin-start", 24,
+                      "margin-bottom", 24,
+                      "spacing", 24,
+                      "homogeneous", TRUE,
+                      "hexpand", TRUE,
+                      NULL);
+  row = g_object_new (ADW_TYPE_PREFERENCES_ROW,
+                      "child", box,
+                      NULL);
+
+  gtk_box_append (box, gtk_button_new_with_label ("System"));
+  gtk_box_append (box, gtk_button_new_with_label ("Light"));
+  gtk_box_append (box, gtk_button_new_with_label ("Dark"));
+
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (box));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      gtk_widget_set_hexpand (child, TRUE);
+      gtk_widget_set_size_request (child, -1, 96);
+    }
+
+  adw_preferences_group_add (pref_group, GTK_WIDGET (row));
+}
+
+static void
+toggle_cb (const char                   *page,
+           const IdePreferenceItemEntry *item,
+           AdwPreferencesGroup          *pref_group,
+           gpointer                      user_data)
+{
+  AdwActionRow *row;
+  GtkSwitch *child;
+
+  child = g_object_new (GTK_TYPE_SWITCH,
+                        "active", TRUE,
+                        "valign", GTK_ALIGN_CENTER,
+                        NULL);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", item->reserved2,
+                      "subtitle", item->reserved3,
+                      "activatable-widget", child,
+                      NULL);
+  adw_preferences_group_add (pref_group, GTK_WIDGET (row));
+  adw_action_row_add_suffix (row, GTK_WIDGET (child));
+}
+
+static void
+spin_cb (const char                   *page,
+          const IdePreferenceItemEntry *item,
+          AdwPreferencesGroup          *pref_group,
+          gpointer                      user_data)
+{
+  AdwActionRow *row;
+  GtkSwitch *child;
+
+  child = g_object_new (GTK_TYPE_SPIN_BUTTON,
+                        "valign", GTK_ALIGN_CENTER,
+                        NULL);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", item->reserved2,
+                      "subtitle", item->reserved3,
+                      "activatable-widget", child,
+                      NULL);
+  adw_preferences_group_add (pref_group, GTK_WIDGET (row));
+  adw_action_row_add_suffix (row, GTK_WIDGET (child));
+}


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