[gnome-builder] libide-gui: port preferences to GTK 4



commit 9cbbb01a0770446f71842f481047d74ef47c7e06
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jul 11 21:55:53 2022 -0700

    libide-gui: port preferences to GTK 4
    
    This provides a new preferences abstraction. I still don't love it, but it
    appears to work for the time being. We have a lot more preferences still
    to port over though so help with that is appreciated.
    
    The preferences window can be in one of three modes (only two are used)
    which is app or project mode. That determines what types of things are
    added to the preferences window.

 src/libide/gui/ide-config-view-addin.c           |   12 +-
 src/libide/gui/ide-config-view-addin.h           |   19 +-
 src/libide/gui/ide-environment-editor-row.c      |    9 +-
 src/libide/gui/ide-environment-editor.c          |   60 +-
 src/libide/gui/ide-environment-editor.h          |    7 +-
 src/libide/gui/ide-preferences-addin.c           |   32 +-
 src/libide/gui/ide-preferences-addin.h           |   32 +-
 src/libide/gui/ide-preferences-builtin-private.h |    4 +-
 src/libide/gui/ide-preferences-builtin.c         |  317 ++++-
 src/libide/gui/ide-preferences-choice-row.c      |  243 ++++
 src/libide/gui/ide-preferences-choice-row.h      |   34 +
 src/libide/gui/ide-preferences-window.c          | 1600 +++++++++++++++++++++-
 src/libide/gui/ide-preferences-window.h          |  144 +-
 src/libide/gui/ide-preferences-window.ui         |  100 +-
 src/libide/gui/libide-gui.h                      |    2 +
 src/libide/gui/meson.build                       |   21 +
 src/libide/gui/tests/meson.build                 |    3 +
 src/libide/gui/tests/test-preferences.c          |  316 +++++
 18 files changed, 2837 insertions(+), 118 deletions(-)
---
diff --git a/src/libide/gui/ide-config-view-addin.c b/src/libide/gui/ide-config-view-addin.c
index 65ebf1492..9cfdc6f26 100644
--- a/src/libide/gui/ide-config-view-addin.c
+++ b/src/libide/gui/ide-config-view-addin.c
@@ -32,15 +32,15 @@ ide_config_view_addin_default_init (IdeConfigViewAddinInterface *iface)
 }
 
 void
-ide_config_view_addin_load (IdeConfigViewAddin *self,
-                            DzlPreferences     *preferences,
-                            IdeConfig   *configuration)
+ide_config_view_addin_load (IdeConfigViewAddin   *self,
+                            IdePreferencesWindow *preferences,
+                            IdeConfig            *config)
 {
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_CONFIG_VIEW_ADDIN (self));
-  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
-  g_return_if_fail (IDE_IS_CONFIG (configuration));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (preferences));
+  g_return_if_fail (IDE_IS_CONFIG (config));
 
   if (IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load)
-    IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load (self, preferences, configuration);
+    IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load (self, preferences, config);
 }
diff --git a/src/libide/gui/ide-config-view-addin.h b/src/libide/gui/ide-config-view-addin.h
index 5e8a2103b..5cddd4e76 100644
--- a/src/libide/gui/ide-config-view-addin.h
+++ b/src/libide/gui/ide-config-view-addin.h
@@ -20,10 +20,15 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
 #include <libide-core.h>
 #include <libide-foundry.h>
 
+#include "ide-preferences-window.h"
+
 G_BEGIN_DECLS
 
 #define IDE_TYPE_CONFIG_VIEW_ADDIN (ide_config_view_addin_get_type())
@@ -35,14 +40,14 @@ struct _IdeConfigViewAddinInterface
 {
   GTypeInterface parent_iface;
 
-  void (*load) (IdeConfigViewAddin *self,
-                DzlPreferences     *preferences,
-                IdeConfig          *configuration);
+  void (*load) (IdeConfigViewAddin   *self,
+                IdePreferencesWindow *preferences,
+                IdeConfig            *config);
 };
 
 IDE_AVAILABLE_IN_ALL
-void ide_config_view_addin_load (IdeConfigViewAddin *self,
-                                 DzlPreferences     *preferences,
-                                 IdeConfig          *configuration);
+void ide_config_view_addin_load (IdeConfigViewAddin   *self,
+                                 IdePreferencesWindow *preferences,
+                                 IdeConfig            *config);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-environment-editor-row.c b/src/libide/gui/ide-environment-editor-row.c
index cdde364b9..12ca5f4a3 100644
--- a/src/libide/gui/ide-environment-editor-row.c
+++ b/src/libide/gui/ide-environment-editor-row.c
@@ -127,9 +127,9 @@ value_entry_activate (GtkWidget               *entry,
 }
 
 static void
-ide_environment_editor_row_destroy (GtkWidget *widget)
+ide_environment_editor_row_dispose (GObject *object)
 {
-  IdeEnvironmentEditorRow *self = (IdeEnvironmentEditorRow *)widget;
+  IdeEnvironmentEditorRow *self = (IdeEnvironmentEditorRow *)object;
 
   if (self->variable != NULL)
     {
@@ -137,7 +137,7 @@ ide_environment_editor_row_destroy (GtkWidget *widget)
       g_clear_object (&self->variable);
     }
 
-  GTK_WIDGET_CLASS (ide_environment_editor_row_parent_class)->destroy (widget);
+  G_OBJECT_CLASS (ide_environment_editor_row_parent_class)->dispose (object);
 }
 
 static void
@@ -184,11 +184,10 @@ ide_environment_editor_row_class_init (IdeEnvironmentEditorRowClass *klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
+  object_class->dispose = ide_environment_editor_row_dispose;
   object_class->get_property = ide_environment_editor_row_get_property;
   object_class->set_property = ide_environment_editor_row_set_property;
 
-  widget_class->destroy = ide_environment_editor_row_destroy;
-
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-environment-editor-row.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, delete_button);
   gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, key_entry);
diff --git a/src/libide/gui/ide-environment-editor.c b/src/libide/gui/ide-environment-editor.c
index 13c016af7..bfe95cfb0 100644
--- a/src/libide/gui/ide-environment-editor.c
+++ b/src/libide/gui/ide-environment-editor.c
@@ -29,14 +29,16 @@
 
 struct _IdeEnvironmentEditor
 {
-  GtkListBox      parent_instance;
-  IdeEnvironment *environment;
-  GtkWidget      *dummy_row;
+  GtkWidget               parent_instance;
+
+  GtkListBox             *list_box;
+  IdeEnvironment         *environment;
+  GtkWidget              *dummy_row;
 
   IdeEnvironmentVariable *dummy;
 };
 
-G_DEFINE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, GTK_TYPE_LIST_BOX)
+G_DEFINE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, GTK_TYPE_WIDGET)
 
 enum {
   PROP_0,
@@ -113,7 +115,7 @@ ide_environment_editor_disconnect (IdeEnvironmentEditor *self)
   g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
   g_assert (IDE_IS_ENVIRONMENT (self->environment));
 
-  gtk_list_box_bind_model (GTK_LIST_BOX (self), NULL, NULL, NULL, NULL);
+  gtk_list_box_bind_model (self->list_box, NULL, NULL, NULL, NULL);
 
   g_clear_object (&self->dummy);
 }
@@ -124,12 +126,12 @@ ide_environment_editor_connect (IdeEnvironmentEditor *self)
   g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
   g_assert (IDE_IS_ENVIRONMENT (self->environment));
 
-  gtk_list_box_bind_model (GTK_LIST_BOX (self),
+  gtk_list_box_bind_model (self->list_box,
                            G_LIST_MODEL (self->environment),
                            ide_environment_editor_create_row, self, NULL);
 
   self->dummy_row = ide_environment_editor_create_dummy_row (self);
-  gtk_container_add (GTK_CONTAINER (self), self->dummy_row);
+  gtk_list_box_append (self->list_box, self->dummy_row);
 }
 
 static void
@@ -167,17 +169,24 @@ find_row (IdeEnvironmentEditor   *self,
   g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
   g_assert (IDE_IS_ENVIRONMENT_VARIABLE (variable));
 
-  gtk_container_foreach (GTK_CONTAINER (self), find_row_cb, &lookup);
+  for (GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (self->list_box));
+       child != NULL;
+       child = gtk_widget_get_next_sibling (child))
+    {
+      find_row_cb (child, &lookup);
+      if (lookup.row)
+        return lookup.row;
+    }
 
   return lookup.row;
 }
 
 static void
-ide_environment_editor_row_activated (GtkListBox    *list_box,
-                                      GtkListBoxRow *row)
+ide_environment_editor_row_activated (IdeEnvironmentEditor *self,
+                                      GtkListBoxRow        *row,
+                                      GtkListBox           *list_box)
 {
-  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)list_box;
-
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
   g_assert (GTK_IS_LIST_BOX (list_box));
   g_assert (GTK_IS_LIST_BOX_ROW (row));
 
@@ -195,13 +204,14 @@ ide_environment_editor_row_activated (GtkListBox    *list_box,
 }
 
 static void
-ide_environment_editor_destroy (GtkWidget *widget)
+ide_environment_editor_dispose (GObject *object)
 {
-  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)widget;
-
-  GTK_WIDGET_CLASS (ide_environment_editor_parent_class)->destroy (widget);
+  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)object;
 
+  g_clear_pointer ((GtkWidget **)&self->list_box, gtk_widget_unparent);
   g_clear_object (&self->environment);
+
+  G_OBJECT_CLASS (ide_environment_editor_parent_class)->dispose (object);
 }
 
 static void
@@ -247,15 +257,11 @@ ide_environment_editor_class_init (IdeEnvironmentEditorClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-  GtkListBoxClass *list_box_class = GTK_LIST_BOX_CLASS (klass);
 
+  object_class->dispose = ide_environment_editor_dispose;
   object_class->get_property = ide_environment_editor_get_property;
   object_class->set_property = ide_environment_editor_set_property;
 
-  widget_class->destroy = ide_environment_editor_destroy;
-
-  list_box_class->row_activated = ide_environment_editor_row_activated;
-
   properties [PROP_ENVIRONMENT] =
     g_param_spec_object ("environment",
                          "Environment",
@@ -264,12 +270,22 @@ ide_environment_editor_class_init (IdeEnvironmentEditorClass *klass)
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
 }
 
 static void
 ide_environment_editor_init (IdeEnvironmentEditor *self)
 {
-  gtk_list_box_set_selection_mode (GTK_LIST_BOX (self), GTK_SELECTION_NONE);
+  self->list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                                 "selection-mode", GTK_SELECTION_NONE,
+                                 NULL);
+  g_signal_connect_object (self->list_box,
+                           "row-activated",
+                           G_CALLBACK (ide_environment_editor_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_widget_set_parent (GTK_WIDGET (self->list_box), GTK_WIDGET (self));
 }
 
 GtkWidget *
diff --git a/src/libide/gui/ide-environment-editor.h b/src/libide/gui/ide-environment-editor.h
index 49cafb079..c999a8bac 100644
--- a/src/libide/gui/ide-environment-editor.h
+++ b/src/libide/gui/ide-environment-editor.h
@@ -20,7 +20,12 @@
 
 #pragma once
 
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
 #include <gtk/gtk.h>
+
 #include <libide-core.h>
 #include <libide-threading.h>
 
@@ -29,7 +34,7 @@ G_BEGIN_DECLS
 #define IDE_TYPE_ENVIRONMENT_EDITOR (ide_environment_editor_get_type())
 
 IDE_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, IDE, ENVIRONMENT_EDITOR, GtkListBox)
+G_DECLARE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, IDE, ENVIRONMENT_EDITOR, GtkWidget)
 
 IDE_AVAILABLE_IN_ALL
 GtkWidget      *ide_environment_editor_new             (void);
diff --git a/src/libide/gui/ide-preferences-addin.c b/src/libide/gui/ide-preferences-addin.c
index eef7dd6c2..96cc97fb7 100644
--- a/src/libide/gui/ide-preferences-addin.c
+++ b/src/libide/gui/ide-preferences-addin.c
@@ -35,46 +35,44 @@ ide_preferences_addin_default_init (IdePreferencesAddinInterface *iface)
  * ide_preferences_addin_load:
  * @self: An #IdePreferencesAddin.
  * @preferences: The preferences container implementation.
+ * @context: (nullable): an #IdeContext or %NULL
  *
  * This interface method is called when a preferences addin is initialized. It
  * could be initialized from multiple preferences implementations, so consumers
- * should use the #DzlPreferences interface to add their preferences controls
- * to the container.
- *
- * Such implementations might include a preferences dialog window, or a
- * preferences widget which could be rendered as a perspective.
- *
- * Since: 3.32
+ * should use the #IdePreferencesWindow to add their preferences controls.
  */
 void
-ide_preferences_addin_load (IdePreferencesAddin *self,
-                            DzlPreferences      *preferences)
+ide_preferences_addin_load (IdePreferencesAddin  *self,
+                            IdePreferencesWindow *preferences,
+                            IdeContext           *context)
 {
   g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
-  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (preferences));
+  g_return_if_fail (!context || IDE_IS_CONTEXT (context));
 
   if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load)
-    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences);
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences, context);
 }
 
 /**
  * ide_preferences_addin_unload:
  * @self: An #IdePreferencesAddin.
  * @preferences: The preferences container implementation.
+ * @context: (nullable): an #IdeContext or %NULL
  *
  * This interface method is called when the preferences addin should remove all
  * controls added to @preferences. This could happen during desctruction of
  * @preferences, or when the plugin is unloaded.
- *
- * Since: 3.32
  */
 void
-ide_preferences_addin_unload (IdePreferencesAddin *self,
-                              DzlPreferences      *preferences)
+ide_preferences_addin_unload (IdePreferencesAddin  *self,
+                              IdePreferencesWindow *preferences,
+                              IdeContext           *context)
 {
   g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
-  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (preferences));
+  g_return_if_fail (!context || IDE_IS_CONTEXT (context));
 
   if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload)
-    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences);
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences, context);
 }
diff --git a/src/libide/gui/ide-preferences-addin.h b/src/libide/gui/ide-preferences-addin.h
index 70fa8f098..b88409045 100644
--- a/src/libide/gui/ide-preferences-addin.h
+++ b/src/libide/gui/ide-preferences-addin.h
@@ -20,32 +20,40 @@
 
 #pragma once
 
-#include <dazzle.h>
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
 
 #include <libide-core.h>
 
+#include "ide-preferences-window.h"
+
 G_BEGIN_DECLS
 
 #define IDE_TYPE_PREFERENCES_ADDIN (ide_preferences_addin_get_type())
 
-IDE_AVAILABLE_IN_3_32
+IDE_AVAILABLE_IN_ALL
 G_DECLARE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, IDE, PREFERENCES_ADDIN, GObject)
 
 struct _IdePreferencesAddinInterface
 {
   GTypeInterface parent_interface;
 
-  void (*load)   (IdePreferencesAddin *self,
-                  DzlPreferences      *preferences);
-  void (*unload) (IdePreferencesAddin *self,
-                  DzlPreferences      *preferences);
+  void (*load)   (IdePreferencesAddin  *self,
+                  IdePreferencesWindow *preferences,
+                  IdeContext           *context);
+  void (*unload) (IdePreferencesAddin  *self,
+                  IdePreferencesWindow *preferences,
+                  IdeContext           *context);
 };
 
-IDE_AVAILABLE_IN_3_32
-void ide_preferences_addin_load   (IdePreferencesAddin *self,
-                                   DzlPreferences      *preferences);
-IDE_AVAILABLE_IN_3_32
-void ide_preferences_addin_unload (IdePreferencesAddin *self,
-                                   DzlPreferences      *preferences);
+IDE_AVAILABLE_IN_ALL
+void ide_preferences_addin_load   (IdePreferencesAddin  *self,
+                                   IdePreferencesWindow *preferences,
+                                   IdeContext           *context);
+IDE_AVAILABLE_IN_ALL
+void ide_preferences_addin_unload (IdePreferencesAddin  *self,
+                                   IdePreferencesWindow *preferences,
+                                   IdeContext           *context);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-builtin-private.h 
b/src/libide/gui/ide-preferences-builtin-private.h
index ca3f8b5be..038d21e6b 100644
--- a/src/libide/gui/ide-preferences-builtin-private.h
+++ b/src/libide/gui/ide-preferences-builtin-private.h
@@ -20,10 +20,10 @@
 
 #pragma once
 
-#include <dazzle.h>
+#include "ide-preferences-window.h"
 
 G_BEGIN_DECLS
 
-void _ide_preferences_builtin_register (DzlPreferences *preferences);
+void _ide_preferences_builtin_register (IdePreferencesWindow *window);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-builtin.c b/src/libide/gui/ide-preferences-builtin.c
index 456d6d575..5a894aead 100644
--- a/src/libide/gui/ide-preferences-builtin.c
+++ b/src/libide/gui/ide-preferences-builtin.c
@@ -22,16 +22,18 @@
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <glib/gi18n.h>
+
+#include <adwaita.h>
 #include <gtksourceview/gtksource.h>
-#include <handy.h>
 #include <libpeas/peas.h>
 
 #include "ide-preferences-builtin-private.h"
-#include "ide-preferences-language-row-private.h"
+#include "ide-style-variant-preview-private.h"
 
-static gint
+static gboolean is_plugin_category (const char *name);
+
+static int
 sort_plugin_info (gconstpointer a,
                   gconstpointer b)
 {
@@ -47,21 +49,18 @@ sort_plugin_info (gconstpointer a,
 }
 
 static void
-ide_preferences_builtin_register_plugins (DzlPreferences *preferences)
+ide_preferences_builtin_add_plugins (IdePreferencesWindow *window)
 {
   PeasEngine *engine;
   const GList *list;
   GList *copy;
   guint i = 0;
 
-  g_assert (DZL_IS_PREFERENCES (preferences));
+  g_assert (IDE_IS_PREFERENCES_WINDOW (window));
 
   engine = peas_engine_get_default ();
   list = peas_engine_get_plugin_list (engine);
 
-  dzl_preferences_add_page (preferences, "plugins", _("Extensions"), 700);
-  dzl_preferences_add_list_group (preferences, "plugins", "plugins", _("Extensions"), GTK_SELECTION_NONE, 
100);
-
   copy = g_list_sort (g_list_copy ((GList *)list), sort_plugin_info);
 
   for (const GList *iter = copy; iter; iter = iter->next, i++)
@@ -69,24 +68,43 @@ ide_preferences_builtin_register_plugins (DzlPreferences *preferences)
       PeasPluginInfo *plugin_info = iter->data;
       g_autofree gchar *path = NULL;
       g_autofree gchar *keywords = NULL;
+      IdePreferenceItemEntry item = {0};
+      const gchar *category;
       const gchar *desc;
       const gchar *name;
+      const gchar *module_name;
 
       if (peas_plugin_info_is_hidden (plugin_info))
         continue;
 
+      category = peas_plugin_info_get_external_data (plugin_info, "Category");
+      if (!is_plugin_category (category))
+        category = "other";
+
+      module_name = peas_plugin_info_get_module_name (plugin_info);
       name = peas_plugin_info_get_name (plugin_info);
       desc = peas_plugin_info_get_description (plugin_info);
       keywords = g_strdup_printf ("%s %s", name, desc);
-      path = g_strdup_printf ("/org/gnome/builder/plugins/%s/",
-                              peas_plugin_info_get_module_name (plugin_info));
-
-      dzl_preferences_add_switch (preferences, "plugins", "plugins", "org.gnome.builder.plugin", "enabled", 
path, NULL, name, desc, keywords, i);
+      path = g_strdup_printf ("/org/gnome/builder/plugins/%s/", module_name);
+
+      item.page = "plugins";
+      item.group = category;
+      item.name = module_name;
+      item.priority = i++;
+      item.callback = ide_preferences_window_toggle;
+      item.title = name;
+      item.subtitle = desc;
+      item.schema_id = "org.gnome.builder.plugin";
+      item.path = path;
+      item.key = "enabled";
+
+      ide_preferences_window_add_items (window, &item, 1, window, NULL);
     }
 
   g_list_free (copy);
 }
 
+#if 0
 static void
 ide_preferences_builtin_register_appearance (DzlPreferences *preferences)
 {
@@ -128,7 +146,7 @@ ide_preferences_builtin_register_appearance (DzlPreferences *preferences)
       dzl_preferences_add_radio (preferences, "appearance", "schemes", "org.gnome.builder.editor", 
"style-scheme-name", NULL, variant_str, title, NULL, title, i);
     }
 
-  if (!hdy_style_manager_get_system_supports_color_schemes (hdy_style_manager_get_default ()))
+  if (!adw_style_manager_get_system_supports_color_schemes (adw_style_manager_get_default ()))
     {
       bin = dzl_preferences_get_widget (preferences, follow_style);
       gtk_widget_set_sensitive (bin, FALSE);
@@ -563,27 +581,266 @@ ide_preferences_builtin_register_vcs (DzlPreferences *preferences)
   g_clear_object (&extensions);
 }
 #endif
+#endif
 
 static void
-ide_preferences_builtin_register_sdks (DzlPreferences *preferences)
+handle_style_variant (const char                   *page_name,
+                      const IdePreferenceItemEntry *entry,
+                      AdwPreferencesGroup          *group,
+                      gpointer                      user_data)
+{
+  static const struct {
+    const char *key;
+    AdwColorScheme color_scheme;
+    const char *title;
+  } variants[] = {
+    { "default", ADW_COLOR_SCHEME_DEFAULT, N_("Follow System") },
+    { "light", ADW_COLOR_SCHEME_FORCE_LIGHT, N_("Light") },
+    { "dark", ADW_COLOR_SCHEME_FORCE_DARK, N_("Dark") },
+  };
+  GtkBox *box;
+  GtkBox *options;
+
+  g_assert (ADW_IS_PREFERENCES_GROUP (group));
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "css-name", "list",
+                      NULL);
+  gtk_widget_add_css_class (GTK_WIDGET (box), "boxed-list");
+  gtk_widget_add_css_class (GTK_WIDGET (box), "style-variant");
+
+  options = g_object_new (GTK_TYPE_BOX,
+                          "halign", GTK_ALIGN_CENTER,
+                          NULL);
+
+  for (guint i = 0; i < G_N_ELEMENTS (variants); i++)
+    {
+      IdeStyleVariantPreview *preview;
+      GtkButton *button;
+      GtkLabel *label;
+      GtkBox *vbox;
+
+      vbox = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_VERTICAL,
+                           "spacing", 8,
+                           "margin-top", 18,
+                           "margin-bottom", 18,
+                           "margin-start", 9,
+                           "margin-end", 9,
+                           NULL);
+      preview = g_object_new (IDE_TYPE_STYLE_VARIANT_PREVIEW,
+                              "color-scheme", variants[i].color_scheme,
+                              NULL);
+      button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                             "action-name", "app.style-variant",
+                             "child", preview,
+                             NULL);
+      gtk_actionable_set_action_target (GTK_ACTIONABLE (button), "s", variants[i].key);
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "xalign", 0.5f,
+                            "label", g_dgettext (NULL, variants[i].title),
+                            NULL);
+      gtk_box_append (vbox, GTK_WIDGET (button));
+      gtk_box_append (vbox, GTK_WIDGET (label));
+      gtk_box_append (options, GTK_WIDGET (vbox));
+    }
+
+  gtk_box_append (box, GTK_WIDGET (options));
+  adw_preferences_group_add (group, GTK_WIDGET (box));
+}
+
+static const IdePreferencePageEntry pages[] = {
+  { NULL, "visual",   "appearance", "preferences-desktop-appearance-symbolic",           0, N_("Appearance") 
},
+  { NULL, "visual",   "editing",    "document-edit-symbolic",                           10, N_("Editing") },
+  { NULL, "visual",   "keyboard",   "preferences-desktop-keyboard-shortcuts-symbolic",  20, N_("Shortcuts") 
},
+  { NULL, "code",     "languages",  "text-x-javascript-symbolic",                      100, N_("Languages") 
},
+  { NULL, "code",     "insight",    "folder-saved-search-symbolic",                    120, N_("Insight") },
+  { NULL, "projects", "projects",   "folder-symbolic",                                 200, N_("Projects") },
+  { NULL, "tools",    "build",      "builder-build-symbolic",                          300, N_("Build") },
+  { NULL, "tools",    "debug",      "builder-debugger-symbolic",                       310, N_("Debugger") },
+  { NULL, "tools",    "commands",   "text-x-script-symbolic",                          320, N_("Commands") },
+  { NULL, "network",  "sdks",       "package-x-generic-symbolic",                      500, N_("SDKs") },
+  { NULL, "network",  "network",    "folder-download-symbolic",                        600, N_("Network") },
+  { NULL, "plugins",  "plugins",    "builder-plugin-symbolic",                         700, N_("Plugins") },
+};
+
+static const IdePreferencePageEntry project_pages[] = {
+  { NULL, "config", "overview",       "info-symbolic",                          0, N_("Overview") },
+  { NULL, "config", "build",          "builder-build-symbolic",               100, N_("Configurations") },
+  { NULL, "code",   "languages",      "text-x-javascript-symbolic",           200, N_("Languages") },
+  { NULL, "tools",  "application",    "builder-run-start-symbolic",           310, N_("Application") },
+  { NULL, "tools",  "commands",       "text-x-script-symbolic",               320, N_("Commands") },
+};
+
+static const IdePreferenceGroupEntry groups[] = {
+  { "appearance", "style",                  0, N_("Appearance") },
+  { "appearance", "interface",           1000, N_("Interface") },
+
+  { "editing",    "completion",             0, N_("Completion") },
+  { "editing",    "formatting",           100, N_("Formatting") },
+  { "editing",    "snippets",             200, N_("Snippets") },
+
+  { "insight",    "general",                0, NULL },
+  { "insight",    "completion",            10, N_("Completion") },
+  { "insight",    "completion-providers",  20, NULL },
+  { "insight",    "diagnostics-providers", 40, N_("Diagnostics") },
+
+  { "plugins",    "vcs",                    0, N_("Version Control") },
+  { "plugins",    "sdks",                  10, N_("SDKs") },
+  { "plugins",    "lsps",                  20, N_("Language Servers") },
+  { "plugins",    "devices",               30, N_("Devices & Simulators") },
+  { "plugins",    "diagnostics",           40, N_("Diagnostics") },
+  { "plugins",    "buildsystems",          50, N_("Build Systems") },
+  { "plugins",    "compilers",             60, N_("Compilers") },
+  { "plugins",    "debuggers",             70, N_("Debuggers") },
+  { "plugins",    "templates",             80, N_("Templates") },
+  { "plugins",    "editing",               90, N_("Editing & Formatting") },
+  { "plugins",    "keybindings",          100, N_("Keyboard Shortcuts") },
+  { "plugins",    "other",                500, N_("Additional") },
+
+  { "keyboard",   "keybindings",            0, N_("Keyboard Shortcuts") },
+
+  { "projects",   "session",                0, N_("Behavior") },
+  { "projects",   "templates",             10, N_("Templates") },
+
+  { "debug",      "breakpoints",            0, N_("Breakpoints") },
+
+  { "build",      "general",                0, N_("General") },
+
+  { "network",    "downloads",              0, N_("Downloads") },
+};
+
+static const IdePreferenceGroupEntry project_groups[] = {
+  { "application", "install",  0, N_("Starting & Stopping") },
+  { "application", "stop",    10 },
+};
+
+static const IdePreferenceItemEntry items[] = {
+  { "appearance", "style", "style-variant", 0, handle_style_variant },
+
+  { "appearance", "interface", "use-tabbar", 0, ide_preferences_window_toggle,
+    N_("Navigate with Tab Bar"),
+    N_("Switch documents using a tabbed interface"),
+    "org.gnome.builder.editor", NULL, "use-tabbar" },
+
+  { "projects", "session", "restore", 0, ide_preferences_window_toggle,
+    N_("Restore Previous Session"),
+    N_("Open previously opened files when loading a project"),
+    "org.gnome.builder", NULL, "restore-previous-files" },
+
+  { "projects", "templates", "default-license", 0, ide_preferences_window_combo,
+    N_("License"),
+    N_("The default license when creating new projects"),
+    "org.gnome.builder", NULL, "default-license" },
+
+  { "debug", "breakpoints", "break-on-main", 0, ide_preferences_window_toggle,
+    N_("Stop After Launching Program"),
+    N_("Automatically insert a breakpoint at the start of the application"),
+    "org.gnome.builder.build", NULL, "debugger-breakpoint-on-main" },
+
+  { "build", "general", "clear-build-logs", 10, ide_preferences_window_toggle,
+    N_("Clear Build Logs"),
+    N_("Upon rebuilding the project the build log will be cleared"),
+    "org.gnome.builder.build", NULL, "clear-build-log-pane" },
+
+  { "build", "general", "clear-build-cache", 20, ide_preferences_window_toggle,
+    N_("Clear Expired Artifacts"),
+    N_("Artifacts which have expired will be deleted when Builder is started"),
+    "org.gnome.builder", NULL, "clear-cache-at-startup" },
+
+  { "network", "downloads", "metered", 0, ide_preferences_window_toggle,
+    N_("Allow Downloads over Metered Connections"),
+    N_("Allow the use of metered network connections when automatically downloading dependencies"),
+    "org.gnome.builder.build", NULL, "allow-network-when-metered" },
+
+  { "keyboard", "keybindings", "default", 0, ide_preferences_window_check,
+    N_("Builder"),
+    N_("Keyboard shortcuts similar to GNOME Text Editor"),
+    "org.gnome.builder.editor", NULL, "keybindings", "'default'" },
+
+  /* TODO: This belongs in plugins/terminal after it is ported */
+  { "appearance", "font", "terminal-font", 10, ide_preferences_window_font,
+    N_("Terminal Font"),
+    N_("The font used within terminals"),
+    "org.gnome.builder.terminal", NULL, "font-name" },
+};
+
+static const IdePreferenceItemEntry project_items[] = {
+  { "application", "install", "install-before-run", 0, ide_preferences_window_toggle,
+    N_("Install Before Running"),
+    N_("Installs the application before running. This is necessary for most projects unless run commands are 
used."),
+    "org.gnome.builder.project", NULL, "install-before-run" },
+
+  { "application", "stop", "stop-signal", 0, ide_preferences_window_combo,
+    N_("Stop Signal"),
+    N_("Send the signal to the target application when requesting the application stop."),
+    "org.gnome.builder.project", NULL, "stop-signal" },
+};
+
+static gboolean
+is_plugin_category (const char *name)
 {
-  /* only the page goes here, plugins will fill in the details */
-  dzl_preferences_add_page (preferences, "sdk", _("SDKs"), 550);
+  static GHashTable *categories;
+
+  if (name == NULL)
+    return FALSE;
+
+  if (categories == NULL)
+    {
+      categories = g_hash_table_new (g_str_hash, g_str_equal);
+      for (guint i = 0; i < G_N_ELEMENTS (groups); i++)
+        {
+          if (strcmp (groups[i].page, "plugins") == 0)
+            g_hash_table_add (categories, (char *)groups[i].name);
+        }
+    }
+
+  return g_hash_table_contains (categories, name);
 }
 
 void
-_ide_preferences_builtin_register (DzlPreferences *preferences)
+_ide_preferences_builtin_register (IdePreferencesWindow *window)
 {
-  ide_preferences_builtin_register_appearance (preferences);
-  ide_preferences_builtin_register_editor (preferences);
-  ide_preferences_builtin_register_languages (preferences);
-  ide_preferences_builtin_register_code_insight (preferences);
-  ide_preferences_builtin_register_completion (preferences);
-  ide_preferences_builtin_register_snippets (preferences);
-  ide_preferences_builtin_register_keyboard (preferences);
-  ide_preferences_builtin_register_plugins (preferences);
-  ide_preferences_builtin_register_build (preferences);
-  ide_preferences_builtin_register_projects (preferences);
-  //ide_preferences_builtin_register_vcs (preferences);
-  ide_preferences_builtin_register_sdks (preferences);
+  IdePreferencesMode mode;
+  IdeContext *context;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (window));
+
+  mode = ide_preferences_window_get_mode (window);
+  context = ide_preferences_window_get_context (window);
+
+  if (mode == IDE_PREFERENCES_MODE_APPLICATION)
+    {
+      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), window, NULL);
+
+      ide_preferences_builtin_add_plugins (window);
+    }
+  else if (mode == IDE_PREFERENCES_MODE_PROJECT)
+    {
+      g_autoptr(GArray) copy = g_array_new (FALSE, FALSE, sizeof (IdePreferenceItemEntry));
+      g_autofree char *project_id = ide_context_dup_project_id (context);
+      g_autofree char *project_settings_path = g_strdup_printf ("/org/gnome/builder/projects/%s/", 
project_id);
+
+      ide_preferences_window_add_pages (window, project_pages, G_N_ELEMENTS (project_pages), NULL);
+      ide_preferences_window_add_groups (window, project_groups, G_N_ELEMENTS (project_groups), NULL);
+
+      for (guint i = 0; i < G_N_ELEMENTS (project_items); i++)
+        {
+          IdePreferenceItemEntry *item;
+
+          g_array_append_val (copy, project_items[i]);
+          item = &g_array_index (copy, IdePreferenceItemEntry, i);
+
+          if (ide_str_equal0 (item->schema_id, "org.gnome.builder.project"))
+            item->path = project_settings_path;
+        }
+
+      ide_preferences_window_add_items (window,
+                                        &g_array_index (copy, IdePreferenceItemEntry, 0),
+                                        copy->len,
+                                        window,
+                                        NULL);
+    }
 }
diff --git a/src/libide/gui/ide-preferences-choice-row.c b/src/libide/gui/ide-preferences-choice-row.c
new file mode 100644
index 000000000..db9567fc1
--- /dev/null
+++ b/src/libide/gui/ide-preferences-choice-row.c
@@ -0,0 +1,243 @@
+/* ide-preferences-choice-row.c
+ *
+ * Copyright 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-preferences-choice-row"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-preferences-choice-row.h"
+
+struct _IdePreferencesChoiceRow
+{
+  AdwComboRow parent_instance;
+  GSettings *settings;
+  char *key;
+};
+
+enum {
+  PROP_0,
+  PROP_KEY,
+  PROP_SETTINGS,
+  N_PROPS
+};
+
+G_DEFINE_FINAL_TYPE (IdePreferencesChoiceRow, ide_preferences_choice_row, ADW_TYPE_COMBO_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+on_selected_changed_cb (IdePreferencesChoiceRow *self,
+                        GParamSpec              *pspec)
+{
+  g_autoptr(GtkStringObject) strobj = NULL;
+  GListModel *model;
+  const char *value;
+  guint selected;
+
+  g_assert (IDE_IS_PREFERENCES_CHOICE_ROW (self));
+
+  selected = adw_combo_row_get_selected (ADW_COMBO_ROW (self));
+  model = adw_combo_row_get_model (ADW_COMBO_ROW (self));
+  strobj = g_list_model_get_item (model, selected);
+  value = gtk_string_object_get_string (strobj);
+
+  g_settings_set_string (self->settings, self->key, value);
+}
+
+static void
+on_settings_changed_cb (IdePreferencesChoiceRow *self,
+                        const char              *key,
+                        GSettings               *settings)
+{
+  g_autofree char *value = NULL;
+  GListModel *model;
+  guint n_items;
+
+  g_assert (IDE_IS_PREFERENCES_CHOICE_ROW (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  model = adw_combo_row_get_model (ADW_COMBO_ROW (self));
+  n_items = g_list_model_get_n_items (model);
+  value = g_settings_get_string (settings, key);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(GtkStringObject) strobj = g_list_model_get_item (model, i);
+      const char *str = gtk_string_object_get_string (strobj);
+
+      if (ide_str_equal0 (str, value))
+        {
+          adw_combo_row_set_selected (ADW_COMBO_ROW (self), i);
+          break;
+        }
+    }
+}
+
+static void
+ide_preferences_choice_row_constructed (GObject *object)
+{
+  IdePreferencesChoiceRow *self = (IdePreferencesChoiceRow *)object;
+  g_autoptr(GSettingsSchemaKey) key = NULL;
+  g_autoptr(GSettingsSchema) schema = NULL;
+  g_autoptr(GListStore) model = NULL;
+  g_autoptr(GVariant) range = NULL;
+  g_autoptr(GVariant) wrapper = NULL;
+  g_autoptr(GVariant) choices = NULL;
+  g_autofree char *current = NULL;
+  g_autofree char *changed_key_name = NULL;
+  GVariantIter iter;
+  const char *choice = NULL;
+  const char *type = NULL;
+
+  G_OBJECT_CLASS (ide_preferences_choice_row_parent_class)->constructed (object);
+
+  if (self->key == NULL || self->settings == NULL)
+    g_return_if_reached ();
+
+  g_object_get (self->settings,
+                "settings-schema", &schema,
+                NULL);
+
+  if (!(key = g_settings_schema_get_key (schema, self->key)))
+    g_return_if_reached ();
+
+  current = g_settings_get_string (self->settings, self->key);
+  range = g_settings_schema_key_get_range (key);
+  g_variant_get (range, "(&s@v)", &type, &wrapper);
+
+  if (!ide_str_equal0 (type, "enum"))
+    {
+      g_warning ("%s must be used with GSettings choice keys",
+                 G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  choices = g_variant_get_variant (wrapper);
+  model = g_list_store_new (GTK_TYPE_STRING_OBJECT);
+
+  g_variant_iter_init (&iter, choices);
+  while (g_variant_iter_next (&iter, "&s", &choice))
+    {
+      g_autoptr(GtkStringObject) strobj = gtk_string_object_new (choice);
+      g_list_store_append (model, strobj);
+    }
+
+  adw_combo_row_set_model (ADW_COMBO_ROW (self), G_LIST_MODEL (model));
+
+  g_signal_connect (self,
+                    "notify::selected",
+                    G_CALLBACK (on_selected_changed_cb),
+                    NULL);
+
+  changed_key_name = g_strdup_printf ("changed::%s", self->key);
+  g_signal_connect_object (self->settings,
+                           changed_key_name,
+                           G_CALLBACK (on_settings_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  on_settings_changed_cb (self, self->key, self->settings);
+}
+
+static void
+ide_preferences_choice_row_dispose (GObject *object)
+{
+  IdePreferencesChoiceRow *self = (IdePreferencesChoiceRow *)object;
+
+  g_clear_object (&self->settings);
+  ide_clear_string (&self->key);
+
+  G_OBJECT_CLASS (ide_preferences_choice_row_parent_class)->dispose (object);
+}
+
+static void
+ide_preferences_choice_row_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  IdePreferencesChoiceRow *self = IDE_PREFERENCES_CHOICE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SETTINGS:
+      g_value_set_object (value, self->settings);
+      break;
+
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_preferences_choice_row_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  IdePreferencesChoiceRow *self = IDE_PREFERENCES_CHOICE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_SETTINGS:
+      self->settings = g_value_dup_object (value);
+      break;
+
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_preferences_choice_row_class_init (IdePreferencesChoiceRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_preferences_choice_row_constructed;
+  object_class->dispose = ide_preferences_choice_row_dispose;
+  object_class->get_property = ide_preferences_choice_row_get_property;
+  object_class->set_property = ide_preferences_choice_row_set_property;
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key", NULL, NULL,
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SETTINGS] =
+    g_param_spec_object ("settings", NULL, NULL,
+                         G_TYPE_SETTINGS,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_preferences_choice_row_init (IdePreferencesChoiceRow *self)
+{
+}
diff --git a/src/libide/gui/ide-preferences-choice-row.h b/src/libide/gui/ide-preferences-choice-row.h
new file mode 100644
index 000000000..79074d118
--- /dev/null
+++ b/src/libide/gui/ide-preferences-choice-row.h
@@ -0,0 +1,34 @@
+/* ide-preferences-choice-row.h
+ *
+ * Copyright 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
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <adwaita.h>
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_CHOICE_ROW (ide_preferences_choice_row_get_type())
+
+IDE_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (IdePreferencesChoiceRow, ide_preferences_choice_row, IDE, PREFERENCES_CHOICE_ROW, 
AdwComboRow)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.c b/src/libide/gui/ide-preferences-window.c
index 29a845bdc..d7ed9eaf7 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,1619 @@
 
 #include "config.h"
 
+#include <glib/gi18n.h>
+
+#include <libide-plugins.h>
+
+#include "ide-gui-enums.h"
+#include "ide-config-view-addin.h"
+#include "ide-preferences-addin.h"
+#include "ide-preferences-builtin-private.h"
+#include "ide-preferences-choice-row.h"
 #include "ide-preferences-window.h"
+#include "ide-workbench.h"
 
 struct _IdePreferencesWindow
 {
-  HdyApplicationWindow parent_window;
+  AdwApplicationWindow parent_window;
+
+  IdePreferencesMode mode;
+
+  IdeExtensionSetAdapter *addins;
+  IdeContext             *context;
+
+  GtkToggleButton    *search_button;
+  GtkButton          *back_button;
+  GtkStack           *page_stack;
+  AdwWindowTitle     *page_title;
+  GtkStack           *pages_stack;
+  AdwWindowTitle     *pages_title;
+
+  GHashTable         *settings;
+
+  const IdePreferencePageEntry *current_page;
+
+  guint rebuild_source;
+
+  struct {
+    GPtrArray *pages;
+    GPtrArray *groups;
+    GPtrArray *items;
+    GArray *data;
+  } info;
+};
+
+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)
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_MODE,
+  N_PROPS
 };
 
-G_DEFINE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, HDY_TYPE_APPLICATION_WINDOW)
+static GParamSpec *properties [N_PROPS];
+
+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 GSettings *
+ide_preferences_window_get_settings (IdePreferencesWindow         *self,
+                                     const IdePreferenceItemEntry *entry)
+{
+  g_autofree char *key = NULL;
+  g_autofree char *path = NULL;
+  GSettings *settings;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (entry->schema_id == NULL)
+    return NULL;
+
+  if (entry->path != NULL &&
+      self->current_page != NULL &&
+      g_str_has_suffix (entry->path, "/*"))
+    {
+      const char *subkey = strrchr (self->current_page->name, '/');
+
+      if (subkey != NULL)
+        {
+          guint j = strlen (entry->path) - 1;
+          char c;
+
+          subkey++;
+
+          path = g_malloc0 (strlen (entry->path) + strlen (subkey) + 2);
+          memcpy (path, entry->path, j);
+          while ((c = *(subkey++)))
+            path[j++] = c;
+          path[j++] = '/';
+          path[j] = 0;
+        }
+    }
+
+  if (path == NULL && entry->path != NULL)
+    path = g_strdup (entry->path);
+
+  if (path == NULL)
+    key = g_strdup_printf ("%s:/", entry->schema_id);
+  else
+    key = g_strdup_printf ("%s:%s", entry->schema_id, path);
+
+  if (!(settings = g_hash_table_lookup (self->settings, key)))
+    {
+      if (path)
+        settings = g_settings_new_with_path (entry->schema_id, path);
+      else
+        settings = g_settings_new (entry->schema_id);
+
+      g_hash_table_insert (self->settings, g_steal_pointer (&key), settings);
+    }
+
+  return settings;
+}
+
+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)))
+    {
+      self->current_page = NULL;
+
+      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_extension_added (IdeExtensionSetAdapter *set,
+                                        PeasPluginInfo         *plugin_info,
+                                        PeasExtension          *exten,
+                                        gpointer                user_data)
+{
+  IdePreferencesWindow *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (IDE_IS_PREFERENCES_ADDIN (exten));
+
+  ide_preferences_addin_load (IDE_PREFERENCES_ADDIN (exten), self, self->context);
+
+  IDE_EXIT;
+}
+
+static void
+ide_preferences_window_extension_removed (IdeExtensionSetAdapter *set,
+                                          PeasPluginInfo         *plugin_info,
+                                          PeasExtension          *exten,
+                                          gpointer                user_data)
+{
+  IdePreferencesWindow *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (IDE_IS_PREFERENCES_ADDIN (exten));
+
+  ide_preferences_addin_unload (IDE_PREFERENCES_ADDIN (exten), self, self->context);
+
+  IDE_EXIT;
+}
+
+static void
+ide_preferences_window_load_addins (IdePreferencesWindow *self)
+{
+  IdeContext *context = NULL;
+  const char *kind = "application";
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (self->addins == NULL);
+
+  _ide_preferences_builtin_register (self);
+
+  if (self->mode == IDE_PREFERENCES_MODE_PROJECT)
+    {
+      IdeWorkbench *workbench = IDE_WORKBENCH (gtk_window_get_group (GTK_WINDOW (self)));
+
+      context = ide_workbench_get_context (workbench);
+      kind = "project";
+    }
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (context),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_PREFERENCES_ADDIN,
+                                                "Preferences-Kind", kind);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_preferences_window_extension_added),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_preferences_window_extension_removed),
+                    self);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_preferences_window_extension_added,
+                                     self);
+}
+
+static void
+ide_preferences_window_dispose (GObject *object)
+{
+  IdePreferencesWindow *self = (IdePreferencesWindow *)object;
+
+  ide_clear_and_destroy_object (&self->addins);
+
+  g_clear_object (&self->context);
+
+  g_clear_pointer (&self->settings, g_hash_table_unref);
+  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_show (GtkWidget *widget)
+{
+  IdePreferencesWindow *self = (IdePreferencesWindow *)widget;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (self->addins == NULL)
+    ide_preferences_window_load_addins (self);
+
+  GTK_WIDGET_CLASS (ide_preferences_window_parent_class)->show (widget);
+}
+
+static void
+ide_preferences_window_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdePreferencesWindow *self = IDE_PREFERENCES_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, self->context);
+      break;
+
+    case PROP_MODE:
+      g_value_set_enum (value, self->mode);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_preferences_window_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdePreferencesWindow *self = IDE_PREFERENCES_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      self->context = g_value_dup_object (value);
+      break;
+
+    case PROP_MODE:
+      self->mode = g_value_get_enum (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
 
 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;
+  object_class->get_property = ide_preferences_window_get_property;
+  object_class->set_property = ide_preferences_window_set_property;
+
+  widget_class->show = ide_preferences_window_show;
+
+  properties [PROP_MODE] =
+    g_param_spec_enum ("mode",
+                       "Mode",
+                       "The mode for the preferences window",
+                       IDE_TYPE_PREFERENCES_MODE,
+                       IDE_PREFERENCES_MODE_EMPTY,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The project context, if any",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/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);
+
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_w, GDK_CONTROL_MASK, "window.close", NULL);
 }
 
 static void
 ide_preferences_window_init (IdePreferencesWindow *self)
 {
+  self->settings = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+  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));
+
   gtk_widget_init_template (GTK_WIDGET (self));
+  gtk_widget_add_css_class (GTK_WIDGET (self), "preferences");
+
+#ifdef DEVELOPMENT_BUILD
+  gtk_widget_add_css_class (GTK_WIDGET (self), "devel");
+#endif
+}
+
+GtkWidget *
+ide_preferences_window_new (IdePreferencesMode  mode,
+                            IdeContext         *context)
+{
+  g_return_val_if_fail (!context || IDE_IS_CONTEXT (context), NULL);
+
+  return g_object_new (IDE_TYPE_PREFERENCES_WINDOW,
+                       "context", context,
+                       "mode", mode,
+                       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 gboolean
+group_is_empty (AdwPreferencesGroup *group)
+{
+  GtkWidget *box;
+  GtkWidget *listbox_box;
+  GtkWidget *listbox;
+
+  g_assert (ADW_IS_PREFERENCES_GROUP (group));
+
+  /* Not exactly awesome that this is hard coded as the implementation
+   * could very well change, but until we have a way to get this out of
+   * AdwPreferencesGroup, this will suffice.
+   */
+  return (box = gtk_widget_get_first_child (GTK_WIDGET (group))) &&
+         GTK_IS_BOX (box) &&
+         (listbox_box = gtk_widget_get_last_child (box)) &&
+         GTK_IS_BOX (listbox_box) &&
+         (listbox = gtk_widget_get_first_child (listbox_box)) &&
+         GTK_IS_LIST_BOX (listbox) &&
+         gtk_widget_get_first_child (listbox) == NULL &&
+         gtk_widget_get_last_child (listbox_box) == listbox;
+}
+
+static char *
+get_project_title (IdePreferencesWindow *self)
+{
+  IdeWorkbench *workbench;
+  IdeContext *context;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (self->mode != IDE_PREFERENCES_MODE_PROJECT)
+    return NULL;
+
+  workbench = IDE_WORKBENCH (gtk_window_get_group (GTK_WINDOW (self)));
+  context = ide_workbench_get_context (workbench);
+
+  return ide_context_dup_title (context);
+}
+
+static void
+ide_preferences_window_set_page_entry (IdePreferencesWindow         *self,
+                                       const IdePreferencePageEntry *entry)
+{
+  g_autofree char *project_title = NULL;
+  const IdePreferencePageEntry *parent;
+  AdwPreferencesPage *page;
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_PREFERENCES_WINDOW (self));
+  g_assert (entry != NULL);
+
+  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 if ((project_title = get_project_title (self)))
+    adw_window_title_set_title (self->pages_title, project_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->user_data);
+            }
+
+          if (!group_is_empty (pref_group))
+            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
+ide_preferences_window_page_activated_cb (IdePreferencesWindow *self,
+                                          GtkListBoxRow        *row,
+                                          GtkListBox           *list_box)
+{
+  const IdePreferencePageEntry *entry;
+
+  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;
+
+  ide_preferences_window_set_page_entry (self, entry);
+}
+
+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));
+
+  g_clear_handle_id (&self->rebuild_source, g_source_remove);
+
+  /* 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_group (IdePreferencesWindow *self,
+                                  const char           *page,
+                                  const char           *name,
+                                  int                   priority,
+                                  const char           *title)
+{
+  IdePreferenceGroupEntry entry = {0};
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  entry.page = page;
+  entry.name = name;
+  entry.priority = priority;
+  entry.title = title;
+
+  ide_preferences_window_add_groups (self, &entry, 1, NULL);
+}
+
+/**
+ * ide_preferences_window_add_groups:
+ * @self: a #IdePreferencesWindow
+ * @groups: (array length=n_groups): the groups to add
+ * @translation_domain: (nullable): gettext translation domain for i18n
+ *
+ * Adds the groups to the preferences window pages.
+ */
+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];
+      g_autofree char *title_esc = NULL;
+      const char *title;
+
+      title = g_dgettext (translation_domain, entry.title);
+      title_esc = g_markup_escape_text (title ? title : "", -1);
+
+      entry.page = g_intern_string (entry.page);
+      entry.name = g_intern_string (entry.name);
+      entry.title = g_intern_string (title_esc);
+
+      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; };
+
+/**
+ * ide_preferences_window_add_items:
+ * @self: a #IdePreferencesWindow
+ * @items: (array length=n_items): an array of items to add
+ * @user_data: (scope async): user data for callbacks
+ * @user_data_destroy: callback to destroy user data
+ *
+ * Adds @items to the preferences window.
+ */
+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.title = g_intern_string (entry.title);
+      entry.subtitle = g_intern_string (entry.subtitle);
+      entry.schema_id = g_intern_string (entry.schema_id);
+      entry.path = g_intern_string (entry.path);
+      entry.key = g_intern_string (entry.key);
+      entry.value = g_intern_string (entry.value);
+      entry.user_data = 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 = {0};
+
+  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.callback = callback;
+  entry.user_data = 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);
+}
+
+/**
+ * ide_preferences_window_add_toggle:
+ * @self: a #IdePreferencesWindow
+ *
+ * Helper to add a toggle.
+ *
+ * This is mostly for use by language bindings such as Python.
+ */
+void
+ide_preferences_window_add_toggle (IdePreferencesWindow         *self,
+                                   const IdePreferenceItemEntry *item)
+{
+  IdePreferenceItemEntry entry;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (item != NULL);
+
+  entry = *item;
+  entry.callback = ide_preferences_window_toggle;
+
+  ide_preferences_window_add_items (self, &entry, 1, self, NULL);
+}
+
+/**
+ * ide_preferences_window_add_spin:
+ * @self: a #IdePreferencesWindow
+ *
+ * Helper to add a spin button.
+ *
+ * This is mostly for use by language bindings such as Python.
+ */
+void
+ide_preferences_window_add_spin (IdePreferencesWindow         *self,
+                                 const IdePreferenceItemEntry *item)
+{
+  IdePreferenceItemEntry entry;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (item != NULL);
+
+  entry = *item;
+  entry.callback = ide_preferences_window_spin;
+
+  ide_preferences_window_add_items (self, &entry, 1, self, NULL);
+}
+
+/**
+ * ide_preferences_window_add_check:
+ * @self: a #IdePreferencesWindow
+ *
+ * Helper to add a check image.
+ *
+ * This is mostly for use by language bindings such as Python.
+ */
+void
+ide_preferences_window_add_check (IdePreferencesWindow         *self,
+                                  const IdePreferenceItemEntry *item)
+{
+  IdePreferenceItemEntry entry;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (item != NULL);
+
+  entry = *item;
+  entry.callback = ide_preferences_window_check;
+
+  ide_preferences_window_add_items (self, &entry, 1, self, NULL);
+}
+
+void
+ide_preferences_window_toggle (const char                   *page_name,
+                               const IdePreferenceItemEntry *entry,
+                               AdwPreferencesGroup          *group,
+                               gpointer                      user_data)
+{
+  IdePreferencesWindow *self = user_data;
+  g_autofree char *title_esc = NULL;
+  g_autofree char *subtitle_esc = NULL;
+  AdwActionRow *row;
+  GtkSwitch *child;
+  GSettings *settings;
+
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (ADW_IS_PREFERENCES_GROUP (group));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (!(settings = ide_preferences_window_get_settings (self, entry)))
+    return;
+
+  title_esc = g_markup_escape_text (entry->title ? entry->title : "", -1);
+  subtitle_esc = g_markup_escape_text (entry->subtitle ? entry->subtitle : "", -1);
+
+  child = g_object_new (GTK_TYPE_SWITCH,
+                        "valign", GTK_ALIGN_CENTER,
+                        NULL);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", title_esc,
+                      "subtitle", subtitle_esc,
+                      "activatable-widget", child,
+                      NULL);
+  adw_preferences_group_add (group, GTK_WIDGET (row));
+  adw_action_row_add_suffix (row, GTK_WIDGET (child));
+
+  g_settings_bind (settings, entry->key, child, "active", G_SETTINGS_BIND_DEFAULT);
+}
+
+static gboolean
+check_get_mapping (GValue   *to,
+                   GVariant *from,
+                   gpointer  user_data)
+{
+  GVariant *expected = user_data;
+
+  if (expected == NULL)
+    {
+      if (g_variant_is_of_type (from, G_VARIANT_TYPE_BOOLEAN))
+        {
+          g_value_set_boolean (to, g_variant_get_boolean (from));
+          return TRUE;
+        }
+
+      return FALSE;
+    }
+
+  if (g_variant_equal (expected, from))
+    g_value_set_boolean (to, TRUE);
+  else
+    g_value_set_boolean (to, FALSE);
+
+  return TRUE;
+}
+
+static GVariant *
+check_set_mapping (const GValue       *from,
+                   const GVariantType *expected_type,
+                   gpointer            user_data)
+{
+  GVariant *expected = user_data;
+
+  if (G_VALUE_HOLDS_BOOLEAN (from))
+    {
+      if (expected != NULL)
+        return g_variant_ref (expected);
+      else if (g_variant_type_equal (expected_type, G_VARIANT_TYPE_BOOLEAN))
+        return g_variant_new_boolean (g_value_get_boolean (from));
+    }
+
+  return NULL;
+}
+
+void
+ide_preferences_window_check (const char                   *page_name,
+                              const IdePreferenceItemEntry *entry,
+                              AdwPreferencesGroup          *group,
+                              gpointer                      user_data)
+{
+  IdePreferencesWindow *self = user_data;
+  g_autofree char *title_esc = NULL;
+  g_autofree char *subtitle_esc = NULL;
+  g_autoptr(GError) error = NULL;
+  AdwActionRow *row;
+  GtkWidget *child;
+  GSettings *settings;
+  GVariant *value;
+
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (ADW_IS_PREFERENCES_GROUP (group));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (!(settings = ide_preferences_window_get_settings (self, entry)))
+    return;
+
+  title_esc = g_markup_escape_text (entry->title ? entry->title : "", -1);
+  subtitle_esc = g_markup_escape_text (entry->subtitle ? entry->subtitle : "", -1);
+
+  child = g_object_new (GTK_TYPE_CHECK_BUTTON,
+                        "valign", GTK_ALIGN_CENTER,
+                        "can-target", FALSE,
+                        NULL);
+  gtk_widget_add_css_class (child, "checkimage");
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", title_esc,
+                      "subtitle", subtitle_esc,
+                      "activatable-widget", child,
+                      NULL);
+  adw_preferences_group_add (group, GTK_WIDGET (row));
+  adw_action_row_add_suffix (row, GTK_WIDGET (child));
+
+  if (entry->value)
+    value = g_variant_parse (NULL, entry->value, NULL, NULL, &error);
+  else
+    value = NULL;
+
+  if (error != NULL)
+    g_warning ("Failed to parse GVariant: %s", error->message);
+
+  g_settings_bind_with_mapping (settings, entry->key, child, "active",
+                                G_SETTINGS_BIND_DEFAULT,
+                                check_get_mapping, check_set_mapping,
+                                value,
+                                value ? (GDestroyNotify)g_variant_unref : NULL);
+}
+
+static void
+set_double_property (gpointer    instance,
+                     const char *property,
+                     GVariant   *value)
+{
+  GValue val = { 0 };
+  double v = 0;
+
+  g_assert (instance != NULL);
+  g_assert (property != NULL);
+  g_assert (value != NULL);
+
+  if (g_variant_is_of_type (value, G_VARIANT_TYPE_DOUBLE))
+    v = g_variant_get_double (value);
+
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT16))
+    v = g_variant_get_int16 (value);
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT16))
+    v = g_variant_get_uint16 (value);
+
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT32))
+    v = g_variant_get_int32 (value);
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32))
+    v = g_variant_get_uint32 (value);
+
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT64))
+    v = g_variant_get_int64 (value);
+  else if (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64))
+    v = g_variant_get_uint64 (value);
+
+  else
+    g_warning ("Unknown variant type: %s\n", (gchar *)g_variant_get_type (value));
+
+  g_value_init (&val, G_TYPE_DOUBLE);
+  g_value_set_double (&val, v);
+  g_object_set_property (instance, property, &val);
+  g_value_unset (&val);
+}
+
+static GtkAdjustment *
+create_adjustment (const char *schema_id,
+                   const char *path,
+                   const char *key,
+                   guint      *digits)
+{
+  g_autoptr(GSettings) settings = NULL;
+  GSettingsSchema *schema = NULL;
+  GSettingsSchemaKey *schema_key = NULL;
+  GtkAdjustment *ret = NULL;
+  GVariant *range = NULL;
+  GVariant *values = NULL;
+  GVariant *lower = NULL;
+  GVariant *upper = NULL;
+  GVariantIter iter;
+  char *type = NULL;
+
+  g_assert (schema_id != NULL);
+
+  if (path)
+    settings = g_settings_new_with_path (schema_id, path);
+  else
+    settings = g_settings_new (schema_id);
+
+  g_object_get (settings, "settings-schema", &schema, NULL);
+  schema_key = g_settings_schema_get_key (schema, key);
+  range = g_settings_schema_key_get_range (schema_key);
+  g_variant_get (range, "(sv)", &type, &values);
+
+  if (!ide_str_equal0 (type, "range") ||
+      (2 != g_variant_iter_init (&iter, values)))
+    goto cleanup;
+
+  lower = g_variant_iter_next_value (&iter);
+  upper = g_variant_iter_next_value (&iter);
+
+  ret = gtk_adjustment_new (0, 0, 0, 1, 10, 0);
+  set_double_property (ret, "lower", lower);
+  set_double_property (ret, "upper", upper);
+
+  if (g_variant_is_of_type (lower, G_VARIANT_TYPE_DOUBLE) ||
+      g_variant_is_of_type (upper, G_VARIANT_TYPE_DOUBLE))
+    {
+      gtk_adjustment_set_step_increment (ret, 0.1);
+      *digits = 2;
+    }
+
+  g_settings_bind (settings, key, ret, "value", G_SETTINGS_BIND_DEFAULT);
+
+cleanup:
+  g_clear_pointer (&schema_key, g_settings_schema_key_unref);
+  g_clear_pointer (&schema, g_settings_schema_unref);
+  g_clear_pointer (&range, g_variant_unref);
+  g_clear_pointer (&lower, g_variant_unref);
+  g_clear_pointer (&upper, g_variant_unref);
+  g_clear_pointer (&values, g_variant_unref);
+  g_clear_pointer (&type, g_free);
+
+  return ret;
+}
+
+void
+ide_preferences_window_spin (const char                   *page_name,
+                             const IdePreferenceItemEntry *entry,
+                             AdwPreferencesGroup          *group,
+                             gpointer                      user_data)
+{
+  IdePreferencesWindow *self = user_data;
+  g_autofree char *title_esc = NULL;
+  g_autofree char *subtitle_esc = NULL;
+  GtkAdjustment *adj = NULL;
+  AdwActionRow *row;
+  GtkWidget *child;
+  GSettings *settings;
+  guint digits = 0;
+
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (ADW_IS_PREFERENCES_GROUP (group));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (!(settings = ide_preferences_window_get_settings (self, entry)))
+    return;
+
+  title_esc = g_markup_escape_text (entry->title ? entry->title : "", -1);
+  subtitle_esc = g_markup_escape_text (entry->subtitle ? entry->subtitle : "", -1);
+  adj = create_adjustment (entry->schema_id, entry->path, entry->key, &digits);
+
+  child = g_object_new (GTK_TYPE_SPIN_BUTTON,
+                        "valign", GTK_ALIGN_CENTER,
+                        "adjustment", adj,
+                        "digits", digits,
+                        NULL);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", title_esc,
+                      "subtitle", subtitle_esc,
+                      "activatable-widget", child,
+                      NULL);
+  adw_preferences_group_add (group, GTK_WIDGET (row));
+  adw_action_row_add_suffix (row, GTK_WIDGET (child));
+
+  g_settings_bind (settings, entry->key, adj, "value", G_SETTINGS_BIND_DEFAULT);
+}
+
+static void
+on_font_response_cb (GtkFontChooserDialog *dialog,
+                     int                   response_id,
+                     GSettings            *settings)
+{
+  const char *key = g_object_get_data (G_OBJECT (dialog), "SETTINGS_KEY");
+  const char *font = gtk_font_chooser_get_font (GTK_FONT_CHOOSER (dialog));
+
+  if (response_id == GTK_RESPONSE_OK)
+    g_settings_set_string (settings, key, font);
+
+  gtk_window_destroy (GTK_WINDOW (dialog));
+}
+
+static void
+on_font_activate (GtkButton *button,
+                  GSettings *settings)
+{
+  const char *key;
+  GtkWidget *dialog;
+  GtkRoot *root;
+
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (G_IS_SETTINGS (settings));
+
+  key = g_object_get_data (G_OBJECT (button), "SETTINGS_KEY");
+
+  root = gtk_widget_get_root (GTK_WIDGET (button));
+  dialog = gtk_font_chooser_dialog_new (_("Select Font"), GTK_WINDOW (root));
+  g_settings_bind (settings, key, dialog, "font", G_SETTINGS_BIND_GET);
+
+  g_signal_connect_object (dialog,
+                           "response",
+                           G_CALLBACK (on_font_response_cb),
+                           settings,
+                           0);
+
+  g_object_set_data (G_OBJECT (dialog),
+                     "SETTINGS_KEY",
+                     (char *)g_intern_string (key));
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+void
+ide_preferences_window_font (const char                   *page_name,
+                             const IdePreferenceItemEntry *entry,
+                             AdwPreferencesGroup          *group,
+                             gpointer                      user_data)
+{
+  IdePreferencesWindow *self = user_data;
+  g_autofree char *title_esc = NULL;
+  g_autofree char *subtitle_esc = NULL;
+  AdwActionRow *row;
+  GSettings *settings;
+  GtkWidget *child;
+
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (ADW_IS_PREFERENCES_GROUP (group));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (!(settings = ide_preferences_window_get_settings (self, entry)))
+    return;
+
+  title_esc = g_markup_escape_text (entry->title ? entry->title : "", -1);
+  subtitle_esc = g_markup_escape_text (entry->subtitle ? entry->subtitle : "", -1);
+
+  child = g_object_new (GTK_TYPE_BUTTON,
+                        "valign", GTK_ALIGN_CENTER,
+                        NULL);
+  row = g_object_new (ADW_TYPE_ACTION_ROW,
+                      "title", title_esc,
+                      "subtitle", subtitle_esc,
+                      "activatable-widget", child,
+                      NULL);
+  adw_action_row_add_suffix (row, child);
+  adw_preferences_group_add (group, GTK_WIDGET (row));
+
+  g_settings_bind (settings, entry->key, child, "label", G_SETTINGS_BIND_GET);
+  g_object_set_data (G_OBJECT (child),
+                     "SETTINGS_KEY",
+                     (char *)g_intern_string (entry->key));
+
+  g_signal_connect_object (child,
+                           "clicked",
+                           G_CALLBACK (on_font_activate),
+                           settings,
+                           0);
+}
+
+void
+ide_preferences_window_combo (const char                   *page_name,
+                              const IdePreferenceItemEntry *entry,
+                              AdwPreferencesGroup          *group,
+                              gpointer                      user_data)
+{
+  IdePreferencesWindow *self = user_data;
+  g_autofree char *title_esc = NULL;
+  g_autofree char *subtitle_esc = NULL;
+  AdwComboRow *row;
+  GSettings *settings;
+
+  g_return_if_fail (entry != NULL);
+  g_return_if_fail (ADW_IS_PREFERENCES_GROUP (group));
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+
+  if (!(settings = ide_preferences_window_get_settings (self, entry)))
+    return;
+
+  title_esc = g_markup_escape_text (entry->title ? entry->title : "", -1);
+  subtitle_esc = g_markup_escape_text (entry->subtitle ? entry->subtitle : "", -1);
+
+  row = g_object_new (IDE_TYPE_PREFERENCES_CHOICE_ROW,
+                      "key", entry->key,
+                      "settings", settings,
+                      "subtitle", subtitle_esc,
+                      "title", title_esc,
+                      NULL);
+  adw_preferences_group_add (group, GTK_WIDGET (row));
+}
+
+IdePreferencesMode
+ide_preferences_window_get_mode (IdePreferencesWindow *self)
+{
+  g_return_val_if_fail (IDE_IS_PREFERENCES_WINDOW (self), 0);
+
+  return self->mode;
+}
+
+/**
+ * ide_preferences_window_get_context:
+ * @self: a #IdePreferencesWindow
+ *
+ * Gets the context for the preferences window, if any.
+ *
+ * This will always return non-%NULL if the mode is %IDE_PREFERENCES_MODE_PROJECT.
+ *
+ * Otherwise, it will only return non-%NULL if the preferences window was
+ * opened while a project is open.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext or %NULL
+ */
+IdeContext *
+ide_preferences_window_get_context (IdePreferencesWindow *self)
+{
+  g_return_val_if_fail (IDE_IS_PREFERENCES_WINDOW (self), NULL);
+
+  return self->context;
+}
+
+void
+ide_preferences_window_set_page (IdePreferencesWindow *self,
+                                 const char           *page)
+{
+  const IdePreferencePageEntry *p;
+
+  g_return_if_fail (IDE_IS_PREFERENCES_WINDOW (self));
+  g_return_if_fail (page != NULL);
+
+  ide_preferences_window_rebuild (self);
+
+  if ((p = get_page (self, page)))
+    ide_preferences_window_set_page_entry (self, p);
 }
diff --git a/src/libide/gui/ide-preferences-window.h b/src/libide/gui/ide-preferences-window.h
index 9b7d374a0..b5878f377 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,149 @@
 
 #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;
+} IdePreferencePageEntry;
+
+typedef struct
+{
+  const char *page;
+  const char *name;
+  int priority;
+  const char *title;
+} IdePreferenceGroupEntry;
+
+struct _IdePreferenceItemEntry
+{
+  const char *page;
+  const char *group;
+  const char *name;
+
+  int priority;
+
+  IdePreferenceCallback callback;
+
+  /* Callback specific data */
+
+  /* Title/Subtitle for helper functions */
+  const char *title;
+  const char *subtitle;
+
+  /* Schema info for helper functions */
+  const char *schema_id;
+  const char *path;
+  const char *key;
+  const char *value;
+
+  /*< private >*/
+  gconstpointer user_data;
+};
+
+typedef enum
+{
+  IDE_PREFERENCES_MODE_EMPTY,
+  IDE_PREFERENCES_MODE_APPLICATION,
+  IDE_PREFERENCES_MODE_PROJECT,
+} IdePreferencesMode;
+
 #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        (IdePreferencesMode             mode,
+                                                       IdeContext                    *context);
+IDE_AVAILABLE_IN_ALL
+IdePreferencesMode  ide_preferences_window_get_mode   (IdePreferencesWindow          *self);
+IDE_AVAILABLE_IN_ALL
+IdeContext         *ide_preferences_window_get_context (IdePreferencesWindow          *self);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_set_page   (IdePreferencesWindow          *self,
+                                                       const char                    *page);
+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_group  (IdePreferencesWindow          *self,
+                                                       const char                    *page,
+                                                       const char                    *name,
+                                                       int                            priority,
+                                                       const char                    *title);
+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);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_add_toggle (IdePreferencesWindow          *self,
+                                                       const IdePreferenceItemEntry  *item);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_add_spin   (IdePreferencesWindow          *self,
+                                                       const IdePreferenceItemEntry  *item);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_add_check  (IdePreferencesWindow          *self,
+                                                       const IdePreferenceItemEntry  *item);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_toggle     (const char                    *page_name,
+                                                       const IdePreferenceItemEntry  *entry,
+                                                       AdwPreferencesGroup           *group,
+                                                       gpointer                       user_data);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_check      (const char                    *page_name,
+                                                       const IdePreferenceItemEntry  *entry,
+                                                       AdwPreferencesGroup           *group,
+                                                       gpointer                       user_data);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_spin       (const char                    *page_name,
+                                                       const IdePreferenceItemEntry  *entry,
+                                                       AdwPreferencesGroup           *group,
+                                                       gpointer                       user_data);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_font       (const char                    *page_name,
+                                                       const IdePreferenceItemEntry  *entry,
+                                                       AdwPreferencesGroup           *group,
+                                                       gpointer                       user_data);
+IDE_AVAILABLE_IN_ALL
+void                ide_preferences_window_combo      (const char                    *page_name,
+                                                       const IdePreferenceItemEntry  *entry,
+                                                       AdwPreferencesGroup           *group,
+                                                       gpointer                       user_data);
 
 G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.ui b/src/libide/gui/ide-preferences-window.ui
index 118a49e10..775e99904 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-right</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">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/libide/gui/libide-gui.h b/src/libide/gui/libide-gui.h
index f29858dcd..57fa3deb7 100644
--- a/src/libide/gui/libide-gui.h
+++ b/src/libide/gui/libide-gui.h
@@ -48,6 +48,8 @@
 # include "ide-pane.h"
 # include "ide-panel-position.h"
 # include "ide-preferences-addin.h"
+# include "ide-preferences-choice-row.h"
+# include "ide-preferences-window.h"
 # include "ide-primary-workspace.h"
 # include "ide-run-button.h"
 # include "ide-search-popover.h"
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
index ed9ee4f76..c0f821ebc 100644
--- a/src/libide/gui/meson.build
+++ b/src/libide/gui/meson.build
@@ -129,6 +129,25 @@ libide_gui_resources = gnome.compile_resources(
 libide_gui_generated_headers += [libide_gui_resources[1]]
 libide_gui_sources += libide_gui_resources
 
+#
+# Enum generation
+#
+
+libide_gui_enum_headers = [
+  'ide-preferences-window.h',
+]
+
+libide_gui_enums = gnome.mkenums_simple('ide-gui-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_gui_enum_headers,
+  install_header: true,
+     install_dir: libide_gui_header_dir,
+)
+libide_gui_generated_headers += [libide_gui_enums[1]]
+libide_gui_sources += libide_gui_enums
+
 
 #
 # Dependencies
@@ -184,3 +203,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..65c9e23c3
--- /dev/null
+++ b/src/libide/gui/tests/meson.build
@@ -0,0 +1,3 @@
+test_preferences = executable('test-preferences', 'test-preferences.c',
+  dependencies: [libide_gui_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..0b362f038
--- /dev/null
+++ b/src/libide/gui/tests/test-preferences.c
@@ -0,0 +1,316 @@
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-gui.h>
+
+#include "ide-gui-resources.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, "Trim Trailing Whitespace", "Upon saving, trailing 
whitepsace from modified lines will be trimmed." },
+  { "languages/*", "general", "overwrite", 0, toggle_cb, "Overwrite Braces", "Overwrite closing braces" },
+  { "languages/*", "general", "insert-matching", 0, toggle_cb, "Insert Matching Brace", "Insert matching 
character for [[(\"'" },
+  { "languages/*", "general", "insert-trailing", 0, toggle_cb, "Insert Trailing Newline", "Ensure files end 
with a newline" },
+
+  { "languages/*", "margins", "show-right-margin", 0, toggle_cb, "Show right margin", "Display a margin in 
the editor to indicate maximum desired width" },
+  { "languages/*", "margins", "right-margin", 0, spin_cb, "Right margin position", "The position of the 
right margin in characters" },
+
+  { "languages/*", "spacing", "before-parens", 0, toggle_cb, "Prefer a space before opening parentheses" },
+  { "languages/*", "spacing", "before-brackets", 0, toggle_cb, "Prefer a space before opening brackets" },
+  { "languages/*", "spacing", "before-braces", 0, toggle_cb, "Prefer a space before opening braces" },
+  { "languages/*", "spacing", "before-angles", 0, toggle_cb, "Prefer a space before opening angles" },
+
+  { "languages/*", "indentation", "tab-width", 0, spin_cb, "Tab width", "Width of a tab character in spaces" 
},
+  { "languages/*", "indentation", "insert-spaces", 0, toggle_cb, "Insert spaces instead of tabs", "Prefer 
spaces over tabs" },
+  { "languages/*", "indentation", "auto-indent", 0, toggle_cb, "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 ();
+
+  g_resources_register (ide_gui_get_resource ());
+
+  main_loop = g_main_loop_new (NULL, FALSE);
+  window = IDE_PREFERENCES_WINDOW (ide_preferences_window_new (IDE_PREFERENCES_MODE_EMPTY, NULL));
+
+  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->title,
+                      "subtitle", item->subtitle,
+                      "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->title,
+                      "subtitle", item->subtitle,
+                      "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]