[libdazzle] prefs: add preferences implementation



commit 86f47f47e54bb5c15e9462de52bf27fe24dfaf7e
Author: Christian Hergert <chergert redhat com>
Date:   Mon Jun 5 18:41:51 2017 -0700

    prefs: add preferences implementation
    
    This is based on Builder's dynamic preferences engine. What Builder
    provides on top of this is the Addin/Interface control to load plugins
    which create these elements automatically (as well as revoke them).
    
    We will stick to keeping that part downstream as it is dependent on the
    design of the application (even if we are opinionated about using libpeas).

 src/dazzle.gresources.xml                        |    8 +
 src/meson.build                                  |   28 ++
 src/prefs/dzl-preferences-bin-private.h          |   33 ++
 src/prefs/dzl-preferences-bin.c                  |  446 ++++++++++++++++++++++
 src/prefs/dzl-preferences-bin.h                  |   55 +++
 src/prefs/dzl-preferences-entry.c                |  231 +++++++++++
 src/prefs/dzl-preferences-entry.h                |   42 ++
 src/prefs/dzl-preferences-entry.ui               |   26 ++
 src/prefs/dzl-preferences-file-chooser-button.c  |  245 ++++++++++++
 src/prefs/dzl-preferences-file-chooser-button.h  |   32 ++
 src/prefs/dzl-preferences-file-chooser-button.ui |   49 +++
 src/prefs/dzl-preferences-flow-box.c             |   36 ++
 src/prefs/dzl-preferences-flow-box.h             |   34 ++
 src/prefs/dzl-preferences-font-button.c          |  327 ++++++++++++++++
 src/prefs/dzl-preferences-font-button.h          |   32 ++
 src/prefs/dzl-preferences-font-button.ui         |   68 ++++
 src/prefs/dzl-preferences-group-private.h        |   52 +++
 src/prefs/dzl-preferences-group.c                |  443 +++++++++++++++++++++
 src/prefs/dzl-preferences-group.h                |   37 ++
 src/prefs/dzl-preferences-group.ui               |   56 +++
 src/prefs/dzl-preferences-language-row.c         |  167 ++++++++
 src/prefs/dzl-preferences-language-row.h         |   32 ++
 src/prefs/dzl-preferences-language-row.ui        |   26 ++
 src/prefs/dzl-preferences-page-private.h         |   43 ++
 src/prefs/dzl-preferences-page.c                 |  185 +++++++++
 src/prefs/dzl-preferences-page.h                 |   39 ++
 src/prefs/dzl-preferences-page.ui                |   11 +
 src/prefs/dzl-preferences-spin-button.c          |  417 ++++++++++++++++++++
 src/prefs/dzl-preferences-spin-button.h          |   36 ++
 src/prefs/dzl-preferences-spin-button.ui         |   56 +++
 src/prefs/dzl-preferences-switch.c               |  439 +++++++++++++++++++++
 src/prefs/dzl-preferences-switch.h               |   32 ++
 src/prefs/dzl-preferences-switch.ui              |   59 +++
 src/prefs/dzl-preferences.c                      |  260 +++++++++++++
 src/prefs/dzl-preferences.h                      |  200 ++++++++++
 35 files changed, 4282 insertions(+), 0 deletions(-)
---
diff --git a/src/dazzle.gresources.xml b/src/dazzle.gresources.xml
index 40af6cf..6a0ce0b 100644
--- a/src/dazzle.gresources.xml
+++ b/src/dazzle.gresources.xml
@@ -4,6 +4,14 @@
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-simple-popover.ui">widgets/dzl-simple-popover.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-empty-state.ui">widgets/dzl-empty-state.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-pill-box.ui">widgets/dzl-pill-box.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-file-chooser-button.ui">prefs/dzl-preferences-file-chooser-button.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-entry.ui">prefs/dzl-preferences-entry.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-font-button.ui">prefs/dzl-preferences-font-button.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-group.ui">prefs/dzl-preferences-group.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-language-row.ui">prefs/dzl-preferences-language-row.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-page.ui">prefs/dzl-preferences-page.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-spin-button.ui">prefs/dzl-preferences-spin-button.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-preferences-switch.ui">prefs/dzl-preferences-switch.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-suggestion-popover.ui">suggestions/dzl-suggestion-popover.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-suggestion-row.ui">suggestions/dzl-suggestion-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="dzl-shortcut-accel-dialog.ui">shortcuts/dzl-shortcut-accel-dialog.ui</file>
diff --git a/src/meson.build b/src/meson.build
index dbc5543..878d0a7 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -73,6 +73,18 @@ libdazzle_public_headers = [
   'panel/dzl-tab-strip.h',
   'panel/dzl-tab.h',
 
+  'prefs/dzl-preferences-bin.h',
+  'prefs/dzl-preferences-entry.h',
+  'prefs/dzl-preferences-file-chooser-button.h',
+  'prefs/dzl-preferences-flow-box.h',
+  'prefs/dzl-preferences-font-button.h',
+  'prefs/dzl-preferences-group.h',
+  'prefs/dzl-preferences-language-row.h',
+  'prefs/dzl-preferences-page.h',
+  'prefs/dzl-preferences-spin-button.h',
+  'prefs/dzl-preferences-switch.h',
+  'prefs/dzl-preferences.h',
+
   'search/dzl-fuzzy-index-builder.h',
   'search/dzl-fuzzy-index-cursor.h',
   'search/dzl-fuzzy-index-match.h',
@@ -197,6 +209,18 @@ libdazzle_public_sources = [
   'panel/dzl-tab-strip.c',
   'panel/dzl-tab.c',
 
+  'prefs/dzl-preferences-bin.c',
+  'prefs/dzl-preferences-entry.c',
+  'prefs/dzl-preferences-file-chooser-button.c',
+  'prefs/dzl-preferences-flow-box.c',
+  'prefs/dzl-preferences-font-button.c',
+  'prefs/dzl-preferences-group.c',
+  'prefs/dzl-preferences-language-row.c',
+  'prefs/dzl-preferences-page.c',
+  'prefs/dzl-preferences-spin-button.c',
+  'prefs/dzl-preferences-switch.c',
+  'prefs/dzl-preferences.c',
+
   'search/dzl-fuzzy-index-builder.c',
   'search/dzl-fuzzy-index-cursor.c',
   'search/dzl-fuzzy-index-match.c',
@@ -294,6 +318,10 @@ libdazzle_sources = [
   'panel/dzl-dock-paned-private.h',
   'panel/dzl-tab-private.h',
 
+  'prefs/dzl-preferences-bin-private.h',
+  'prefs/dzl-preferences-group-private.h',
+  'prefs/dzl-preferences-page-private.h',
+
   'search/dzl-fuzzy-index-private.h',
 
   'shortcuts/dzl-shortcut-closure-chain.c',
diff --git a/src/prefs/dzl-preferences-bin-private.h b/src/prefs/dzl-preferences-bin-private.h
new file mode 100644
index 0000000..fbb835c
--- /dev/null
+++ b/src/prefs/dzl-preferences-bin-private.h
@@ -0,0 +1,33 @@
+/* dzl-preferences-bin-private.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_BIN_PRIVATE_H
+#define DZL_PREFERENCES_BIN_PRIVATE_H
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+void     _dzl_preferences_bin_set_map (DzlPreferencesBin *self,
+                                       GHashTable        *map);
+gboolean _dzl_preferences_bin_matches (DzlPreferencesBin *self,
+                                       DzlPatternSpec    *spec);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_BIN_PRIVATE_H */
diff --git a/src/prefs/dzl-preferences-bin.c b/src/prefs/dzl-preferences-bin.c
new file mode 100644
index 0000000..fad3a15
--- /dev/null
+++ b/src/prefs/dzl-preferences-bin.c
@@ -0,0 +1,446 @@
+/* dzl-preferences-bin.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "prefs/dzl-preferences-bin.h"
+
+typedef struct
+{
+  GtkBin      parent_instance;
+
+  gint        priority;
+
+  gchar      *keywords;
+  gchar      *schema_id;
+  gchar      *path;
+  GSettings  *settings;
+  GHashTable *map;
+} DzlPreferencesBinPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (DzlPreferencesBin, dzl_preferences_bin, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_KEYWORDS,
+  PROP_PRIORITY,
+  PROP_SCHEMA_ID,
+  PROP_PATH,
+  LAST_PROP
+};
+
+enum {
+  PREFERENCE_ACTIVATED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static GParamSpec *properties [LAST_PROP];
+static GHashTable *settings_cache;
+
+static gchar *
+dzl_preferences_bin_expand (DzlPreferencesBin *self,
+                            const gchar       *spec)
+{
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+  GHashTableIter iter;
+  const gchar *key;
+  const gchar *value;
+  gchar *expanded;
+
+  g_assert (DZL_IS_PREFERENCES_BIN (self));
+
+  if (spec == NULL)
+    return NULL;
+
+  expanded = g_strdup (spec);
+
+  if (priv->map == NULL)
+    goto validate;
+
+  g_hash_table_iter_init (&iter, priv->map);
+
+  while (g_hash_table_iter_next (&iter, (gpointer *)&key, (gpointer *)&value))
+    {
+      gchar *tmp = expanded;
+      gchar **split;
+
+      split = g_strsplit (tmp, key, 0);
+      expanded = g_strjoinv (value, split);
+
+      g_strfreev (split);
+      g_free (tmp);
+    }
+
+validate:
+  if (strchr (expanded, '{') != NULL)
+    {
+      g_free (expanded);
+      return NULL;
+    }
+
+  return expanded;
+}
+
+static void
+dzl_preferences_bin_evict_settings (gpointer  data,
+                                          GObject  *where_object_was)
+{
+  g_assert (data != NULL);
+  g_assert (where_object_was != NULL);
+
+  g_hash_table_remove (settings_cache, (gchar *)data);
+}
+
+static void
+dzl_preferences_bin_cache_settings (const gchar *hash_key,
+                                    GSettings   *settings)
+{
+  gchar *key;
+
+  g_assert (hash_key != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  key = g_strdup (hash_key);
+  g_hash_table_insert (settings_cache, key, settings);
+  g_object_weak_ref (G_OBJECT (settings), dzl_preferences_bin_evict_settings, key);
+}
+
+static GSettings *
+dzl_preferences_bin_get_settings (DzlPreferencesBin *self)
+{
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_BIN (self), NULL);
+
+  if (priv->settings == NULL)
+    {
+      g_autofree gchar *resolved_schema_id = NULL;
+      g_autofree gchar *resolved_path = NULL;
+      g_autofree gchar *hash_key = NULL;
+
+      resolved_schema_id = dzl_preferences_bin_expand (self, priv->schema_id);
+      resolved_path = dzl_preferences_bin_expand (self, priv->path);
+
+      if (resolved_schema_id == NULL)
+        return NULL;
+
+      if ((priv->path != NULL) && (resolved_path == NULL))
+        return NULL;
+
+      hash_key = g_strdup_printf ("%s|%s",
+                                  resolved_schema_id,
+                                  resolved_path ?: "");
+
+      if (!g_hash_table_contains (settings_cache, hash_key))
+        {
+          GSettingsSchemaSource *source;
+          GSettingsSchema *schema;
+
+          source = g_settings_schema_source_get_default ();
+          schema = g_settings_schema_source_lookup (source, resolved_schema_id, TRUE);
+
+          if (schema != NULL)
+            {
+              if (resolved_path)
+                priv->settings = g_settings_new_with_path (resolved_schema_id, resolved_path);
+              else
+                priv->settings = g_settings_new (resolved_schema_id);
+              dzl_preferences_bin_cache_settings (hash_key, priv->settings);
+            }
+
+          g_clear_pointer (&schema, g_settings_schema_unref);
+        }
+      else
+        {
+          priv->settings = g_object_ref (g_hash_table_lookup (settings_cache, hash_key));
+        }
+
+      g_clear_pointer (&hash_key, g_free);
+      g_clear_pointer (&resolved_schema_id, g_free);
+      g_clear_pointer (&resolved_path, g_free);
+    }
+
+  return (priv->settings != NULL) ? g_object_ref (priv->settings) : NULL;
+}
+
+
+static void
+dzl_preferences_bin_connect (DzlPreferencesBin *self,
+                             GSettings         *settings)
+{
+  g_assert (DZL_IS_PREFERENCES_BIN (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (DZL_PREFERENCES_BIN_GET_CLASS (self)->connect != NULL)
+    DZL_PREFERENCES_BIN_GET_CLASS (self)->connect (self, settings);
+}
+
+static void
+dzl_preferences_bin_disconnect (DzlPreferencesBin *self,
+                                GSettings         *settings)
+{
+  g_assert (DZL_IS_PREFERENCES_BIN (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (DZL_PREFERENCES_BIN_GET_CLASS (self)->disconnect != NULL)
+    DZL_PREFERENCES_BIN_GET_CLASS (self)->disconnect (self, settings);
+}
+
+static void
+dzl_preferences_bin_reload (DzlPreferencesBin *self)
+{
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+  GSettings *settings;
+
+  g_assert (DZL_IS_PREFERENCES_BIN (self));
+
+  if (priv->settings != NULL)
+    {
+      dzl_preferences_bin_disconnect (self, priv->settings);
+      g_clear_object (&priv->settings);
+    }
+
+  settings = dzl_preferences_bin_get_settings (self);
+
+  if (settings != NULL)
+    {
+      dzl_preferences_bin_connect (self, settings);
+      g_object_unref (settings);
+    }
+}
+
+static void
+dzl_preferences_bin_constructed (GObject *object)
+{
+  DzlPreferencesBin *self = (DzlPreferencesBin *)object;
+
+  G_OBJECT_CLASS (dzl_preferences_bin_parent_class)->constructed (object);
+
+  dzl_preferences_bin_reload (self);
+}
+
+static void
+dzl_preferences_bin_destroy (GtkWidget *widget)
+{
+  DzlPreferencesBin *self = (DzlPreferencesBin *)widget;
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  g_assert (DZL_IS_PREFERENCES_BIN (self));
+
+  if (priv->settings != NULL)
+    {
+      dzl_preferences_bin_disconnect (self, priv->settings);
+      g_clear_object (&priv->settings);
+    }
+
+  GTK_WIDGET_CLASS (dzl_preferences_bin_parent_class)->destroy (widget);
+}
+
+static void
+dzl_preferences_bin_activated (GtkWidget *widget)
+{
+  GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget));
+
+  if (child)
+    gtk_widget_activate (child);
+}
+
+static void
+dzl_preferences_bin_finalize (GObject *object)
+{
+  DzlPreferencesBin *self = (DzlPreferencesBin *)object;
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  g_clear_pointer (&priv->schema_id, g_free);
+  g_clear_pointer (&priv->path, g_free);
+  g_clear_pointer (&priv->keywords, g_free);
+  g_clear_pointer (&priv->map, g_hash_table_unref);
+  g_clear_object (&priv->settings);
+
+  G_OBJECT_CLASS (dzl_preferences_bin_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_bin_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  DzlPreferencesBin *self = DZL_PREFERENCES_BIN (object);
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_SCHEMA_ID:
+      g_value_set_string (value, priv->schema_id);
+      break;
+
+    case PROP_PATH:
+      g_value_set_string (value, priv->path);
+      break;
+
+    case PROP_KEYWORDS:
+      g_value_set_string (value, priv->keywords);
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, priv->priority);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_bin_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  DzlPreferencesBin *self = DZL_PREFERENCES_BIN (object);
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_SCHEMA_ID:
+      priv->schema_id = g_value_dup_string (value);
+      break;
+
+    case PROP_PATH:
+      priv->path = g_value_dup_string (value);
+      break;
+
+    case PROP_KEYWORDS:
+      priv->keywords = g_value_dup_string (value);
+      break;
+
+    case PROP_PRIORITY:
+      priv->priority = g_value_get_int (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_bin_class_init (DzlPreferencesBinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = dzl_preferences_bin_constructed;
+  object_class->finalize = dzl_preferences_bin_finalize;
+  object_class->get_property = dzl_preferences_bin_get_property;
+  object_class->set_property = dzl_preferences_bin_set_property;
+
+  widget_class->destroy = dzl_preferences_bin_destroy;
+
+  properties [PROP_KEYWORDS] =
+    g_param_spec_string ("keywords",
+                         "Keywords",
+                         "Search keywords for the widget.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PATH] =
+    g_param_spec_string ("path",
+                         "Path",
+                         "Path",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The widget priority within the group.",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SCHEMA_ID] =
+    g_param_spec_string ("schema-id",
+                         "Schema Id",
+                         "Schema Id",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [PREFERENCE_ACTIVATED] = widget_class->activate_signal =
+    g_signal_new_class_handler ("preference-activated",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (dzl_preferences_bin_activated),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_css_name (widget_class, "preferencesbin");
+
+  settings_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static void
+dzl_preferences_bin_init (DzlPreferencesBin *self)
+{
+}
+
+void
+_dzl_preferences_bin_set_map (DzlPreferencesBin *self,
+                              GHashTable        *map)
+{
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  g_return_if_fail (DZL_IS_PREFERENCES_BIN (self));
+
+  if (map != priv->map)
+    {
+      g_clear_pointer (&priv->map, g_hash_table_unref);
+      priv->map = map ? g_hash_table_ref (map) : NULL;
+      dzl_preferences_bin_reload (self);
+    }
+}
+
+gboolean
+_dzl_preferences_bin_matches (DzlPreferencesBin *self,
+                              DzlPatternSpec    *spec)
+{
+  DzlPreferencesBinPrivate *priv = dzl_preferences_bin_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_BIN (self), FALSE);
+
+  if (spec == NULL)
+    return TRUE;
+
+  if (priv->keywords && dzl_pattern_spec_match (spec, priv->keywords))
+    return TRUE;
+
+  if (priv->schema_id && dzl_pattern_spec_match (spec, priv->schema_id))
+    return TRUE;
+
+  if (priv->path && dzl_pattern_spec_match (spec, priv->path))
+    return TRUE;
+
+  if (DZL_PREFERENCES_BIN_GET_CLASS (self)->matches)
+    return DZL_PREFERENCES_BIN_GET_CLASS (self)->matches (self, spec);
+
+  return FALSE;
+}
diff --git a/src/prefs/dzl-preferences-bin.h b/src/prefs/dzl-preferences-bin.h
new file mode 100644
index 0000000..cb7d9d9
--- /dev/null
+++ b/src/prefs/dzl-preferences-bin.h
@@ -0,0 +1,55 @@
+/* dzl-preferences-bin.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_BIN_H
+#define DZL_PREFERENCES_BIN_H
+
+#include <gtk/gtk.h>
+
+#include "search/dzl-pattern-spec.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_BIN (dzl_preferences_bin_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DzlPreferencesBin, dzl_preferences_bin, DZL, PREFERENCES_BIN, GtkBin)
+
+struct _DzlPreferencesBinClass
+{
+  GtkBinClass parent_class;
+
+  void     (*connect)    (DzlPreferencesBin *self,
+                          GSettings         *settings);
+  void     (*disconnect) (DzlPreferencesBin *self,
+                          GSettings         *settings);
+  gboolean (*matches)    (DzlPreferencesBin *self,
+                          DzlPatternSpec    *spec);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_BIN_H */
diff --git a/src/prefs/dzl-preferences-entry.c b/src/prefs/dzl-preferences-entry.c
new file mode 100644
index 0000000..94b593b
--- /dev/null
+++ b/src/prefs/dzl-preferences-entry.c
@@ -0,0 +1,231 @@
+/* dzl-preferences-entry.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "prefs/dzl-preferences-entry.h"
+
+typedef struct
+{
+  GtkEntry *entry;
+  GtkLabel *title;
+} DzlPreferencesEntryPrivate;
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_TEXT,
+  LAST_PROP
+};
+
+enum {
+  ACTIVATE,
+  CHANGED,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (DzlPreferencesEntry, dzl_preferences_entry, DZL_TYPE_PREFERENCES_BIN)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static void
+dzl_preferences_entry_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  DzlPreferencesEntry *self = DZL_PREFERENCES_ENTRY (object);
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TEXT:
+      g_value_set_string (value, gtk_entry_get_text (priv->entry));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_text (priv->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_entry_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  DzlPreferencesEntry *self = DZL_PREFERENCES_ENTRY (object);
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TEXT:
+      gtk_entry_set_text (priv->entry, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (priv->title, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_entry_activate (DzlPreferencesEntry *self)
+{
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  g_assert (DZL_IS_PREFERENCES_ENTRY (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (priv->entry));
+}
+
+static void
+dzl_preferences_entry_changed (DzlPreferencesEntry *self,
+                               GtkEntry            *entry)
+{
+  const gchar *text;
+
+  g_assert (DZL_IS_PREFERENCES_ENTRY (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  text = gtk_entry_get_text (entry);
+  g_signal_emit (self, signals [CHANGED], 0, text);
+}
+
+static gboolean
+dzl_preferences_entry_matches (DzlPreferencesBin *bin,
+                               DzlPatternSpec    *spec)
+{
+  DzlPreferencesEntry *self = (DzlPreferencesEntry *)bin;
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+  const gchar *tmp;
+
+  g_assert (DZL_IS_PREFERENCES_ENTRY (self));
+  g_assert (spec != NULL);
+
+  tmp = gtk_label_get_label (priv->title);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  tmp = gtk_entry_get_text (GTK_ENTRY (priv->entry));
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+dzl_preferences_entry_class_init (DzlPreferencesEntryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass);
+
+  object_class->get_property = dzl_preferences_entry_get_property;
+  object_class->set_property = dzl_preferences_entry_set_property;
+
+  bin_class->matches = dzl_preferences_entry_matches;
+
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (dzl_preferences_entry_activate),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  signals [CHANGED] =
+    g_signal_new_class_handler ("changed",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_STRING);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-entry.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesEntry, entry);
+  gtk_widget_class_bind_template_child_private (widget_class, DzlPreferencesEntry, title);
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TEXT] =
+    g_param_spec_string ("text",
+                         "Text",
+                         "Text",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+dzl_preferences_entry_init (DzlPreferencesEntry *self)
+{
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (priv->entry,
+                           "changed",
+                           G_CALLBACK (dzl_preferences_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+/**
+ * dzl_preferences_entry_get_title_widget:
+ *
+ * Returns: (transfer none): A #GtkWidget
+ */
+GtkWidget *
+dzl_preferences_entry_get_title_widget (DzlPreferencesEntry *self)
+{
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_ENTRY (self), NULL);
+
+  return GTK_WIDGET (priv->title);
+}
+
+/**
+ * dzl_preferences_entry_get_entry_widget:
+ *
+ * Returns: (transfer none): A #GtkWidget
+ */
+GtkWidget *
+dzl_preferences_entry_get_entry_widget (DzlPreferencesEntry *self)
+{
+  DzlPreferencesEntryPrivate *priv = dzl_preferences_entry_get_instance_private (self);
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_ENTRY (self), NULL);
+
+  return GTK_WIDGET (priv->entry);
+}
diff --git a/src/prefs/dzl-preferences-entry.h b/src/prefs/dzl-preferences-entry.h
new file mode 100644
index 0000000..2203fd1
--- /dev/null
+++ b/src/prefs/dzl-preferences-entry.h
@@ -0,0 +1,42 @@
+/* dzl-preferences-entry.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_ENTRY_H
+#define DZL_PREFERENCES_ENTRY_H
+
+#include <gtk/gtk.h>
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_ENTRY (dzl_preferences_entry_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (DzlPreferencesEntry, dzl_preferences_entry, DZL, PREFERENCES_ENTRY, 
DzlPreferencesBin)
+
+struct _DzlPreferencesEntryClass
+{
+  DzlPreferencesBinClass parent_class;
+};
+
+GtkWidget *dzl_preferences_entry_get_entry_widget (DzlPreferencesEntry *self);
+GtkWidget *dzl_preferences_entry_get_title_widget (DzlPreferencesEntry *self);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_ENTRY_H */
diff --git a/src/prefs/dzl-preferences-entry.ui b/src/prefs/dzl-preferences-entry.ui
new file mode 100644
index 0000000..55f10ba
--- /dev/null
+++ b/src/prefs/dzl-preferences-entry.ui
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesEntry" parent="DzlPreferencesBin">
+    <child>
+      <object class="GtkBox">
+        <property name="spacing">12</property>
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkEntry" id="entry">
+            <property name="has-frame">false</property>
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-file-chooser-button.c b/src/prefs/dzl-preferences-file-chooser-button.c
new file mode 100644
index 0000000..86b25f4
--- /dev/null
+++ b/src/prefs/dzl-preferences-file-chooser-button.c
@@ -0,0 +1,245 @@
+/* dzl-preferences-file-chooser-button.c
+ *
+ * Copyright (C) 2016 Akshaya Kakkilaya <akshaya kakkilaya gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "util/dzl-util-private.h"
+#include "prefs/dzl-preferences-file-chooser-button.h"
+
+struct _DzlPreferencesFileChooserButton
+{
+  DzlPreferencesBin    parent_instance;
+
+  gchar                *key;
+  GSettings            *settings;
+
+  GtkFileChooserButton *widget;
+  GtkLabel             *title;
+  GtkLabel             *subtitle;
+};
+
+G_DEFINE_TYPE (DzlPreferencesFileChooserButton, dzl_preferences_file_chooser_button, 
DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_ACTION,
+  PROP_KEY,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+dzl_preferences_file_chooser_button_save_file (DzlPreferencesFileChooserButton *self,
+                                               GtkFileChooserButton            *widget)
+{
+  g_autofree gchar *path = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self));
+
+  path = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->widget));
+
+  g_settings_set_string (self->settings, self->key, path);
+
+}
+
+static void
+dzl_preferences_file_chooser_button_connect (DzlPreferencesBin *bin,
+                                             GSettings         *settings)
+{
+  DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)bin;
+  g_autofree gchar *file = NULL;
+  g_autofree gchar *path = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  self->settings = g_object_ref (settings);
+
+  file = g_settings_get_string (settings, self->key);
+
+  if (!dzl_str_empty0 (file))
+    {
+      if (!g_path_is_absolute (file))
+        path = g_build_filename (g_get_home_dir (), file, NULL);
+      else
+        path = g_steal_pointer (&file);
+
+      gtk_file_chooser_set_filename (GTK_FILE_CHOOSER (self->widget), path);
+    }
+
+  g_signal_connect_object (self->widget,
+                           "file-set",
+                           G_CALLBACK (dzl_preferences_file_chooser_button_save_file),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static gboolean
+dzl_preferences_file_chooser_button_matches (DzlPreferencesBin *bin,
+                                             DzlPatternSpec    *spec)
+{
+  DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)bin;
+  const gchar *tmp;
+
+  g_assert (DZL_IS_PREFERENCES_FILE_CHOOSER_BUTTON (self));
+  g_assert (spec != NULL);
+
+  tmp = gtk_label_get_label (self->title);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  tmp = gtk_label_get_label (self->subtitle);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  if (self->key && dzl_pattern_spec_match (spec, self->key))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+dzl_preferences_file_chooser_button_finalize (GObject *object)
+{
+  DzlPreferencesFileChooserButton *self = (DzlPreferencesFileChooserButton *)object;
+
+  g_clear_pointer (&self->key, g_free);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (dzl_preferences_file_chooser_button_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_file_chooser_button_get_property (GObject    *object,
+                                                  guint       prop_id,
+                                                  GValue     *value,
+                                                  GParamSpec *pspec)
+{
+  DzlPreferencesFileChooserButton *self = DZL_PREFERENCES_FILE_CHOOSER_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTION:
+      g_value_set_enum (value, gtk_file_chooser_get_action (GTK_FILE_CHOOSER (self->widget)));
+      break;
+
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_file_chooser_button_set_property (GObject      *object,
+                                                  guint         prop_id,
+                                                  const GValue *value,
+                                                  GParamSpec   *pspec)
+{
+  DzlPreferencesFileChooserButton *self = DZL_PREFERENCES_FILE_CHOOSER_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTION:
+      gtk_file_chooser_set_action (GTK_FILE_CHOOSER (self->widget), g_value_get_enum (value));
+      break;
+
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE:
+      gtk_label_set_label (self->subtitle, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_file_chooser_button_class_init (DzlPreferencesFileChooserButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_file_chooser_button_finalize;
+  object_class->get_property = dzl_preferences_file_chooser_button_get_property;
+  object_class->set_property = dzl_preferences_file_chooser_button_set_property;
+
+  bin_class->connect = dzl_preferences_file_chooser_button_connect;
+  bin_class->matches = dzl_preferences_file_chooser_button_matches;
+
+  properties [PROP_ACTION] =
+    g_param_spec_enum ("action",
+                       "Action",
+                       "Action",
+                       GTK_TYPE_FILE_CHOOSER_ACTION,
+                       GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "Key",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "Subtitle",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-file-chooser-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, widget);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, title);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFileChooserButton, subtitle);
+}
+
+static void
+dzl_preferences_file_chooser_button_init (DzlPreferencesFileChooserButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/prefs/dzl-preferences-file-chooser-button.h b/src/prefs/dzl-preferences-file-chooser-button.h
new file mode 100644
index 0000000..d87c3b4
--- /dev/null
+++ b/src/prefs/dzl-preferences-file-chooser-button.h
@@ -0,0 +1,32 @@
+/* dzl-preferences-file-chooser-button.h
+ *
+ * Copyright (C) 2016 Akshaya Kakkilaya <akshaya kakkilaya gmail com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H
+#define DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_FILE_CHOOSER_BUTTON (dzl_preferences_file_chooser_button_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesFileChooserButton, dzl_preferences_file_chooser_button, DZL, 
PREFERENCES_FILE_CHOOSER_BUTTON, DzlPreferencesBin)
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_FILE_CHOOSER_BUTTON_H */
diff --git a/src/prefs/dzl-preferences-file-chooser-button.ui 
b/src/prefs/dzl-preferences-file-chooser-button.ui
new file mode 100644
index 0000000..0ac6142
--- /dev/null
+++ b/src/prefs/dzl-preferences-file-chooser-button.ui
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesFileChooserButton" parent="DzlPreferencesBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="expand">true</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel" id="title">
+                <property name="vexpand">true</property>
+                <property name="halign">start</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="subtitle">
+                <property name="vexpand">true</property>
+                <property name="halign">start</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.83333"/>
+                </attributes>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkFileChooserButton" id="widget">
+            <property name="expand">true</property>
+            <property name="halign">end</property>
+            <property name="valign">center</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-flow-box.c b/src/prefs/dzl-preferences-flow-box.c
new file mode 100644
index 0000000..31904c5
--- /dev/null
+++ b/src/prefs/dzl-preferences-flow-box.c
@@ -0,0 +1,36 @@
+/* dzl-preferences-flow-box.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "prefs/dzl-preferences-flow-box.h"
+
+struct _DzlPreferencesFlowBox
+{
+  DzlColumnLayout parent;
+};
+
+G_DEFINE_TYPE (DzlPreferencesFlowBox, dzl_preferences_flow_box, DZL_TYPE_COLUMN_LAYOUT)
+
+static void
+dzl_preferences_flow_box_class_init (DzlPreferencesFlowBoxClass *klass)
+{
+}
+
+static void
+dzl_preferences_flow_box_init (DzlPreferencesFlowBox *self)
+{
+}
diff --git a/src/prefs/dzl-preferences-flow-box.h b/src/prefs/dzl-preferences-flow-box.h
new file mode 100644
index 0000000..ca1417a
--- /dev/null
+++ b/src/prefs/dzl-preferences-flow-box.h
@@ -0,0 +1,34 @@
+/* dzl-preferences-flow-box.h
+ *
+ * Copyright (C) 2016 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/>.
+ */
+
+#ifndef DZL_PREFERENCES_FLOW_BOX_H
+#define DZL_PREFERENCES_FLOW_BOX_H
+
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_FLOW_BOX (dzl_preferences_flow_box_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesFlowBox, dzl_preferences_flow_box, DZL, PREFERENCES_FLOW_BOX, 
DzlColumnLayout)
+
+GtkWidget *dzl_preferences_flow_box_new (void);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_FLOW_BOX_H */
diff --git a/src/prefs/dzl-preferences-font-button.c b/src/prefs/dzl-preferences-font-button.c
new file mode 100644
index 0000000..3cd3dd6
--- /dev/null
+++ b/src/prefs/dzl-preferences-font-button.c
@@ -0,0 +1,327 @@
+/* dzl-preferences-font-button.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "prefs/dzl-preferences-font-button.h"
+
+struct _DzlPreferencesFontButton
+{
+  GtkBin                parent_instance;
+
+  gulong                handler;
+
+  GSettings            *settings;
+  gchar                *key;
+
+  GtkLabel             *title;
+  GtkLabel             *font_family;
+  GtkLabel             *font_size;
+  GtkPopover           *popover;
+  GtkButton            *confirm;
+  GtkFontChooserWidget *chooser;
+};
+
+G_DEFINE_TYPE (DzlPreferencesFontButton, dzl_preferences_font_button, DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_KEY,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+enum {
+  ACTIVATE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static void
+dzl_preferences_font_button_show (DzlPreferencesFontButton *self)
+{
+  gchar *font = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+
+  font = g_settings_get_string (self->settings, self->key);
+  g_object_set (self->chooser, "font", font, NULL);
+  g_free (font);
+
+  gtk_popover_popup (self->popover);
+}
+
+static void
+dzl_preferences_font_button_activate (DzlPreferencesFontButton *self)
+{
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (self->popover)))
+    dzl_preferences_font_button_show (self);
+}
+
+static void
+dzl_preferences_font_button_changed (DzlPreferencesFontButton *self,
+                                     const gchar              *key,
+                                     GSettings                *settings)
+{
+  PangoFontDescription *font_desc;
+  gchar *name;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+  g_assert (key != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  name = g_settings_get_string (settings, key);
+  font_desc = pango_font_description_from_string (name);
+
+  if (font_desc != NULL)
+    {
+      gchar *font_size;
+
+      gtk_label_set_label (self->font_family, pango_font_description_get_family (font_desc));
+      font_size = g_strdup_printf ("%d", pango_font_description_get_size (font_desc) / PANGO_SCALE);
+      gtk_label_set_label (self->font_size, font_size);
+      g_free (font_size);
+    }
+
+  g_clear_pointer (&font_desc, pango_font_description_free);
+  g_free (name);
+}
+
+static void
+dzl_preferences_font_button_connect (DzlPreferencesBin *bin,
+                                     GSettings         *settings)
+{
+  DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin;
+  g_autofree gchar *signal_detail = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+
+  signal_detail = g_strdup_printf ("changed::%s", self->key);
+
+  self->settings = g_object_ref (settings);
+
+  self->handler =
+    g_signal_connect_object (settings,
+                             signal_detail,
+                             G_CALLBACK (dzl_preferences_font_button_changed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  dzl_preferences_font_button_changed (self, self->key, settings);
+}
+
+static void
+dzl_preferences_font_button_disconnect (DzlPreferencesBin *bin,
+                                        GSettings         *settings)
+{
+  DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+
+  g_signal_handler_disconnect (settings, self->handler);
+  self->handler = 0;
+}
+
+static gboolean
+dzl_preferences_font_button_matches (DzlPreferencesBin *bin,
+                                     DzlPatternSpec    *spec)
+{
+  DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)bin;
+  const gchar *tmp;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+  g_assert (spec != NULL);
+
+  tmp = gtk_label_get_label (self->title);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  tmp = gtk_label_get_label (self->font_family);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+dzl_preferences_font_button_finalize (GObject *object)
+{
+  DzlPreferencesFontButton *self = (DzlPreferencesFontButton *)object;
+
+  g_clear_object (&self->settings);
+  g_clear_pointer (&self->key, g_free);
+
+  G_OBJECT_CLASS (dzl_preferences_font_button_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_font_button_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  DzlPreferencesFontButton *self = DZL_PREFERENCES_FONT_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_font_button_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  DzlPreferencesFontButton *self = DZL_PREFERENCES_FONT_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_font_button_class_init (DzlPreferencesFontButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_font_button_finalize;
+  object_class->get_property = dzl_preferences_font_button_get_property;
+  object_class->set_property = dzl_preferences_font_button_set_property;
+
+  bin_class->connect = dzl_preferences_font_button_connect;
+  bin_class->disconnect = dzl_preferences_font_button_disconnect;
+  bin_class->matches = dzl_preferences_font_button_matches;
+
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (dzl_preferences_font_button_activate),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "Key",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-font-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, chooser);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, confirm);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, font_family);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, font_size);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, popover);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesFontButton, title);
+}
+
+static gboolean
+transform_to (GBinding     *binding,
+              const GValue *value,
+              GValue       *to_value,
+              gpointer      user_data)
+{
+  g_value_set_boolean (to_value, !!g_value_get_boxed (value));
+  return TRUE;
+}
+
+static void
+dzl_preferences_font_button_clicked (DzlPreferencesFontButton *self,
+                                     GtkButton                *button)
+{
+  g_autofree gchar *font = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  g_object_get (self->chooser, "font", &font, NULL);
+  g_settings_set_string (self->settings, self->key, font);
+  gtk_popover_popdown (self->popover);
+}
+
+static void
+dzl_preferences_font_button_font_activated (DzlPreferencesFontButton *self,
+                                            const gchar              *font,
+                                            GtkFontChooser           *chooser)
+{
+  g_assert (DZL_IS_PREFERENCES_FONT_BUTTON (self));
+  g_assert (GTK_IS_FONT_CHOOSER (chooser));
+
+  g_settings_set_string (self->settings, self->key, font);
+  gtk_popover_popdown (self->popover);
+}
+
+static void
+dzl_preferences_font_button_init (DzlPreferencesFontButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_object_bind_property_full (self->chooser, "font-desc",
+                               self->confirm, "sensitive",
+                               G_BINDING_SYNC_CREATE,
+                               transform_to,
+                               NULL, NULL, NULL);
+
+  g_signal_connect_object (self->chooser,
+                           "font-activated",
+                           G_CALLBACK (dzl_preferences_font_button_font_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->confirm,
+                           "clicked",
+                           G_CALLBACK (dzl_preferences_font_button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/prefs/dzl-preferences-font-button.h b/src/prefs/dzl-preferences-font-button.h
new file mode 100644
index 0000000..45c02bb
--- /dev/null
+++ b/src/prefs/dzl-preferences-font-button.h
@@ -0,0 +1,32 @@
+/* dzl-preferences-font-button.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_FONT_BUTTON_H
+#define DZL_PREFERENCES_FONT_BUTTON_H
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_FONT_BUTTON (dzl_preferences_font_button_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesFontButton, dzl_preferences_font_button, DZL, PREFERENCES_FONT_BUTTON, 
DzlPreferencesBin)
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_FONT_BUTTON_H */
diff --git a/src/prefs/dzl-preferences-font-button.ui b/src/prefs/dzl-preferences-font-button.ui
new file mode 100644
index 0000000..8ede7b8
--- /dev/null
+++ b/src/prefs/dzl-preferences-font-button.ui
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesFontButton" parent="DzlPreferencesBin">
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">18</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="font_family">
+            <property name="visible">true</property>
+            <property name="xalign">1.0</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="font_size">
+            <property name="visible">true</property>
+            <property name="width-chars">2</property>
+            <property name="xalign">0.0</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkPopover" id="popover">
+    <property name="relative-to">box</property>
+    <property name="position">bottom</property>
+    <property name="width-request">600</property>
+    <property name="border-width">12</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkFontChooserWidget" id="chooser">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton" id="confirm">
+            <property name="halign">end</property>
+            <property name="label" translatable="yes">_Select</property>
+            <property name="use-underline">true</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="suggested-action"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/prefs/dzl-preferences-group-private.h b/src/prefs/dzl-preferences-group-private.h
new file mode 100644
index 0000000..f86dde4
--- /dev/null
+++ b/src/prefs/dzl-preferences-group-private.h
@@ -0,0 +1,52 @@
+/* dzl-preferences-group-private.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef DZL_PREFERENCES_GROUP_PRIVATE_H
+#define DZL_PREFERENCES_GROUP_PRIVATE_H
+
+#include "prefs/dzl-preferences-group.h"
+#include "search/dzl-pattern-spec.h"
+
+G_BEGIN_DECLS
+
+struct _DzlPreferencesGroup
+{
+  GtkBin      parent_instance;
+
+  gint        priority;
+  guint       is_list : 1;
+
+  GtkLabel   *title;
+  GtkBox     *box;
+  GtkListBox *list_box;
+  GtkFrame   *list_box_frame;
+
+  GPtrArray  *widgets;
+
+  GtkListBoxRow *last_focused;
+  guint          last_focused_tab_backward : 1;
+};
+
+void  _dzl_preferences_group_set_map  (DzlPreferencesGroup *self,
+                                       GHashTable          *map);
+guint _dzl_preferences_group_refilter (DzlPreferencesGroup *self,
+                                       DzlPatternSpec      *spec);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_GROUP_PRIVATE_H */
diff --git a/src/prefs/dzl-preferences-group.c b/src/prefs/dzl-preferences-group.c
new file mode 100644
index 0000000..959cc92
--- /dev/null
+++ b/src/prefs/dzl-preferences-group.c
@@ -0,0 +1,443 @@
+/* dzl-preferences-group.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "prefs/dzl-preferences-bin.h"
+#include "prefs/dzl-preferences-bin-private.h"
+#include "prefs/dzl-preferences-entry.h"
+#include "prefs/dzl-preferences-group.h"
+#include "prefs/dzl-preferences-group-private.h"
+
+G_DEFINE_TYPE (DzlPreferencesGroup, dzl_preferences_group, GTK_TYPE_BIN)
+
+#define COLUMN_WIDTH 500
+
+enum {
+  PROP_0,
+  PROP_IS_LIST,
+  PROP_MODE,
+  PROP_PRIORITY,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+gint
+dzl_preferences_group_get_priority (DzlPreferencesGroup *self)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), 0);
+
+  return self->priority;
+}
+
+static void
+dzl_preferences_group_widget_destroy (DzlPreferencesGroup *self,
+                                      GtkWidget           *widget)
+{
+  g_assert (DZL_IS_PREFERENCES_GROUP (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  g_ptr_array_remove (self->widgets, widget);
+}
+
+static void
+dzl_preferences_group_row_activated (DzlPreferencesGroup *self,
+                                     GtkListBoxRow       *row,
+                                     GtkListBox          *list_box)
+{
+  GtkWidget *child;
+
+  g_assert (DZL_IS_PREFERENCES_GROUP (self));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  child = gtk_bin_get_child (GTK_BIN (row));
+  if (child != NULL)
+    gtk_widget_activate (child);
+}
+
+static void
+dzl_preferences_group_row_selected (DzlPreferencesGroup *self,
+                                    GtkListBoxRow       *row,
+                                    GtkListBox          *list_box)
+{
+  g_assert (DZL_IS_PREFERENCES_GROUP (self));
+  g_assert (!row || GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if (gtk_list_box_get_selection_mode (list_box) == GTK_SELECTION_SINGLE &&
+      GTK_IS_LIST_BOX_ROW (row) &&
+      gtk_list_box_row_get_activatable (row))
+    dzl_preferences_group_row_activated (self, row, list_box);
+}
+
+const gchar *
+dzl_preferences_group_get_title (DzlPreferencesGroup *self)
+{
+  const gchar *title;
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), NULL);
+
+  title = gtk_label_get_label (self->title);
+
+  return (!title || !*title) ? NULL : title;
+}
+
+static void
+dzl_preferences_group_get_preferred_width (GtkWidget *widget,
+                                           gint      *min_width,
+                                           gint      *nat_width)
+{
+  *min_width = *nat_width = COLUMN_WIDTH;
+}
+
+static void
+dzl_preferences_group_get_preferred_height_for_width (GtkWidget *widget,
+                                                      gint       width,
+                                                      gint      *min_height,
+                                                      gint      *nat_height)
+{
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (min_height != NULL);
+  g_assert (nat_height != NULL);
+
+  GTK_WIDGET_CLASS (dzl_preferences_group_parent_class)->get_preferred_height_for_width (widget, width, 
min_height, nat_height);
+}
+
+static GtkSizeRequestMode
+dzl_preferences_group_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+dzl_preferences_group_finalize (GObject *object)
+{
+  DzlPreferencesGroup *self = (DzlPreferencesGroup *)object;
+
+  g_clear_pointer (&self->widgets, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (dzl_preferences_group_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_group_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  DzlPreferencesGroup *self = DZL_PREFERENCES_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      g_value_set_enum (value, gtk_list_box_get_selection_mode (self->list_box));
+      break;
+
+    case PROP_IS_LIST:
+      g_value_set_boolean (value, self->is_list);
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, self->priority);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_group_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  DzlPreferencesGroup *self = DZL_PREFERENCES_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      gtk_list_box_set_selection_mode (self->list_box, g_value_get_enum (value));
+      break;
+
+    case PROP_IS_LIST:
+      self->is_list = g_value_get_boolean (value);
+      gtk_widget_set_visible (GTK_WIDGET (self->box), !self->is_list);
+      gtk_widget_set_visible (GTK_WIDGET (self->list_box_frame), self->is_list);
+      break;
+
+    case PROP_PRIORITY:
+      self->priority = g_value_get_int (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      gtk_widget_set_visible (GTK_WIDGET (self->title), !!g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_group_class_init (DzlPreferencesGroupClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_group_finalize;
+  object_class->get_property = dzl_preferences_group_get_property;
+  object_class->set_property = dzl_preferences_group_set_property;
+
+  widget_class->get_preferred_width = dzl_preferences_group_get_preferred_width;
+  widget_class->get_preferred_height_for_width = dzl_preferences_group_get_preferred_height_for_width;
+  widget_class->get_request_mode = dzl_preferences_group_get_request_mode;
+
+  properties [PROP_MODE] =
+    g_param_spec_enum ("mode",
+                       NULL,
+                       NULL,
+                       GTK_TYPE_SELECTION_MODE,
+                       GTK_SELECTION_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IS_LIST] =
+    g_param_spec_boolean ("is-list",
+                          "Is List",
+                          "If the group should be rendered as a listbox.",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "Priority",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-group.ui");
+  gtk_widget_class_set_css_name (widget_class, "preferencesgroup");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, box);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, list_box);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, list_box_frame);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesGroup, title);
+}
+
+static void
+dzl_preferences_group_init (DzlPreferencesGroup *self)
+{
+  self->widgets = g_ptr_array_new ();
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->list_box,
+                           "row-activated",
+                           G_CALLBACK (dzl_preferences_group_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->list_box,
+                           "row-selected",
+                           G_CALLBACK (dzl_preferences_group_row_selected),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static gboolean
+dzl_preferences_group_row_focus (DzlPreferencesGroup *self,
+                                 GtkDirectionType     dir,
+                                 GtkListBoxRow       *row)
+{
+  GtkWidget *child;
+  GtkWidget *entry;
+
+  self->last_focused_tab_backward = (dir == GTK_DIR_TAB_BACKWARD);
+
+  child = gtk_bin_get_child (GTK_BIN (row));
+
+  if (DZL_IS_PREFERENCES_ENTRY (child))
+    {
+      entry = dzl_preferences_entry_get_entry_widget ( DZL_PREFERENCES_ENTRY (child));
+      if (GTK_IS_ENTRY (entry) &&
+          gtk_widget_is_focus (entry) &&
+          dir == GTK_DIR_TAB_BACKWARD)
+        gtk_widget_grab_focus (GTK_WIDGET (row));
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+dzl_preferences_group_row_grab_focus (DzlPreferencesGroup *self,
+                                      GtkListBoxRow       *row)
+{
+  GtkWidget *child;
+  GtkListBoxRow *last_focused;
+
+  last_focused = self->last_focused;
+  child = gtk_bin_get_child (GTK_BIN (row));
+  if (DZL_IS_PREFERENCES_ENTRY (child))
+    {
+      self->last_focused = row;
+      if (row != last_focused || !self->last_focused_tab_backward)
+        gtk_widget_activate (child);
+
+      return;
+    }
+
+  self->last_focused = NULL;
+}
+
+void
+dzl_preferences_group_add (DzlPreferencesGroup *self,
+                           GtkWidget           *widget)
+{
+  gint position = -1;
+
+  g_return_if_fail (DZL_IS_PREFERENCES_GROUP (self));
+  g_return_if_fail (DZL_IS_PREFERENCES_BIN (widget));
+
+  g_ptr_array_add (self->widgets, widget);
+
+  g_signal_connect_object (widget,
+                           "destroy",
+                           G_CALLBACK (dzl_preferences_group_widget_destroy),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  if (self->is_list)
+    {
+      GtkWidget *row;
+
+      if (GTK_IS_LIST_BOX_ROW (widget))
+        row = widget;
+      else
+        row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                            "child", widget,
+                            "visible", TRUE,
+                            NULL);
+
+      gtk_container_add (GTK_CONTAINER (self->list_box), row);
+      g_signal_connect_object (row,
+                               "focus",
+                               G_CALLBACK (dzl_preferences_group_row_focus),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_signal_connect_object (row,
+                               "grab-focus",
+                               G_CALLBACK (dzl_preferences_group_row_grab_focus),
+                               self,
+                               G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+    }
+  else
+    {
+      gtk_container_add_with_properties (GTK_CONTAINER (self->box), widget,
+                                         "position", position,
+                                         NULL);
+    }
+}
+
+void
+_dzl_preferences_group_set_map (DzlPreferencesGroup *self,
+                                GHashTable          *map)
+{
+  guint i;
+
+  g_return_if_fail (DZL_IS_PREFERENCES_GROUP (self));
+
+  for (i = 0; i < self->widgets->len; i++)
+    {
+      GtkWidget *widget = g_ptr_array_index (self->widgets, i);
+
+      if (DZL_IS_PREFERENCES_BIN (widget))
+        _dzl_preferences_bin_set_map (DZL_PREFERENCES_BIN (widget), map);
+    }
+}
+
+static void
+dzl_preferences_group_refilter_cb (GtkWidget *widget,
+                                   gpointer   user_data)
+{
+  DzlPreferencesBin *bin = NULL;
+  struct {
+    DzlPatternSpec *spec;
+    guint matches;
+  } *lookup = user_data;
+  gboolean matches;
+
+  if (DZL_IS_PREFERENCES_BIN (widget))
+    bin = DZL_PREFERENCES_BIN (widget);
+  else if (GTK_IS_BIN (widget) && DZL_IS_PREFERENCES_BIN (gtk_bin_get_child (GTK_BIN (widget))))
+    bin = DZL_PREFERENCES_BIN (gtk_bin_get_child (GTK_BIN (widget)));
+  else
+    return;
+
+  if (lookup->spec == NULL)
+    matches = TRUE;
+  else
+    matches = _dzl_preferences_bin_matches (bin, lookup->spec);
+
+  gtk_widget_set_visible (widget, matches);
+
+  lookup->matches += matches;
+}
+
+guint
+_dzl_preferences_group_refilter (DzlPreferencesGroup *self,
+                                 DzlPatternSpec      *spec)
+{
+  struct {
+    DzlPatternSpec *spec;
+    guint matches;
+  } lookup = { spec, 0 };
+  const gchar *tmp;
+
+  g_return_val_if_fail (DZL_IS_PREFERENCES_GROUP (self), 0);
+
+  tmp = gtk_label_get_label (self->title);
+  if (spec && tmp && dzl_pattern_spec_match (spec, tmp))
+    lookup.spec = NULL;
+
+  gtk_container_foreach (GTK_CONTAINER (self->list_box),
+                         dzl_preferences_group_refilter_cb,
+                         &lookup);
+  gtk_container_foreach (GTK_CONTAINER (self->box),
+                         dzl_preferences_group_refilter_cb,
+                         &lookup);
+
+  gtk_widget_set_visible (GTK_WIDGET (self), lookup.matches > 0);
+
+  return lookup.matches;
+}
diff --git a/src/prefs/dzl-preferences-group.h b/src/prefs/dzl-preferences-group.h
new file mode 100644
index 0000000..2c0debf
--- /dev/null
+++ b/src/prefs/dzl-preferences-group.h
@@ -0,0 +1,37 @@
+/* dzl-preferences-group.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_GROUP_H
+#define DZL_PREFERENCES_GROUP_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_GROUP (dzl_preferences_group_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesGroup, dzl_preferences_group, DZL, PREFERENCES_GROUP, GtkBin)
+
+void         dzl_preferences_group_add          (DzlPreferencesGroup *self,
+                                                 GtkWidget           *widget);
+const gchar *dzl_preferences_group_get_title    (DzlPreferencesGroup *self);
+gint         dzl_preferences_group_get_priority (DzlPreferencesGroup *self);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_GROUP_H */
diff --git a/src/prefs/dzl-preferences-group.ui b/src/prefs/dzl-preferences-group.ui
new file mode 100644
index 0000000..5c81526
--- /dev/null
+++ b/src/prefs/dzl-preferences-group.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesGroup" parent="GtkBin">
+    <property name="vexpand">false</property>
+    <property name="halign">fill</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">8</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible">true</property>
+            <property name="hexpand">true</property>
+            <property name="xalign">0.0</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox" id="box">
+                <property name="orientation">vertical</property>
+                <property name="spacing">12</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkFrame" id="list_box_frame">
+                <child>
+                  <object class="DzlScrolledWindow">
+                    <!-- propagate-natural-height on GtkScrolledWindow doesn't work for
+                         us because it doesn't seem to take into account HfW properly. -->
+                    <property name="visible">true</property>
+                    <property name="shadow-type">none</property>
+                    <child>
+                      <object class="GtkListBox" id="list_box">
+                        <property name="selection-mode">none</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-language-row.c b/src/prefs/dzl-preferences-language-row.c
new file mode 100644
index 0000000..aede7e5
--- /dev/null
+++ b/src/prefs/dzl-preferences-language-row.c
@@ -0,0 +1,167 @@
+/* dzl-preferences-language-row.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "prefs/dzl-preferences.h"
+#include "prefs/dzl-preferences-language-row.h"
+
+struct _DzlPreferencesLanguageRow
+{
+  DzlPreferencesBin parent_instance;
+  gchar *id;
+  GtkLabel *title;
+};
+
+G_DEFINE_TYPE (DzlPreferencesLanguageRow, dzl_preferences_language_row, DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+enum {
+  ACTIVATE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static void
+dzl_preferences_language_row_activate (DzlPreferencesLanguageRow *self)
+{
+  GtkWidget *preferences;
+  GHashTable *map;
+
+  g_assert (DZL_IS_PREFERENCES_LANGUAGE_ROW (self));
+
+  if (self->id == NULL)
+    return;
+
+  preferences = gtk_widget_get_ancestor (GTK_WIDGET (self), DZL_TYPE_PREFERENCES);
+  if (preferences == NULL)
+    return;
+
+  map = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+  g_hash_table_insert (map, "{id}", g_strdup (self->id));
+  dzl_preferences_set_page (DZL_PREFERENCES (preferences), "languages.id", map);
+  g_hash_table_unref (map);
+}
+
+static void
+dzl_preferences_language_row_get_property (GObject    *object,
+                                           guint       prop_id,
+                                           GValue     *value,
+                                           GParamSpec *pspec)
+{
+  DzlPreferencesLanguageRow *self = DZL_PREFERENCES_LANGUAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, self->id);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_language_row_set_property (GObject      *object,
+                                           guint         prop_id,
+                                           const GValue *value,
+                                           GParamSpec   *pspec)
+{
+  DzlPreferencesLanguageRow *self = DZL_PREFERENCES_LANGUAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      self->id = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_language_row_finalize (GObject *object)
+{
+  DzlPreferencesLanguageRow *self = (DzlPreferencesLanguageRow *)object;
+
+  g_clear_pointer (&self->id, g_free);
+
+  G_OBJECT_CLASS (dzl_preferences_language_row_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_language_row_class_init (DzlPreferencesLanguageRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_language_row_finalize;
+  object_class->get_property = dzl_preferences_language_row_get_property;
+  object_class->set_property = dzl_preferences_language_row_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "Id",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_FIRST,
+                                G_CALLBACK (dzl_preferences_language_row_activate),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 0);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-language-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesLanguageRow, title);
+}
+
+static void
+dzl_preferences_language_row_init (DzlPreferencesLanguageRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/prefs/dzl-preferences-language-row.h b/src/prefs/dzl-preferences-language-row.h
new file mode 100644
index 0000000..5500001
--- /dev/null
+++ b/src/prefs/dzl-preferences-language-row.h
@@ -0,0 +1,32 @@
+/* dzl-preferences-language-row.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef DZL_PREFERENCES_LANGUAGE_ROW_H
+#define DZL_PREFERENCES_LANGUAGE_ROW_H
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_LANGUAGE_ROW (dzl_preferences_language_row_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesLanguageRow, dzl_preferences_language_row, DZL, 
PREFERENCES_LANGUAGE_ROW, DzlPreferencesBin)
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_LANGUAGE_ROW_H */
diff --git a/src/prefs/dzl-preferences-language-row.ui b/src/prefs/dzl-preferences-language-row.ui
new file mode 100644
index 0000000..9eee6f7
--- /dev/null
+++ b/src/prefs/dzl-preferences-language-row.ui
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesLanguageRow" parent="DzlPreferencesBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">pan-end-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-page-private.h b/src/prefs/dzl-preferences-page-private.h
new file mode 100644
index 0000000..25d14db
--- /dev/null
+++ b/src/prefs/dzl-preferences-page-private.h
@@ -0,0 +1,43 @@
+/* dzl-preferences-page-private.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#ifndef DZL_PREFERENCES_PAGE_PRIVATE_H
+#define DZL_PREFERENCES_PAGE_PRIVATE_H
+
+#include "prefs/dzl-preferences-page.h"
+#include "prefs/dzl-preferences-flow-box.h"
+#include "search/dzl-pattern-spec.h"
+
+G_BEGIN_DECLS
+
+struct _DzlPreferencesPage
+{
+  GtkBin                 parent_instance;
+  gint                   priority;
+  DzlPreferencesFlowBox *box;
+  GHashTable            *groups_by_name;
+};
+
+void _dzl_preferences_page_set_map  (DzlPreferencesPage *self,
+                                     GHashTable         *map);
+void _dzl_preferences_page_refilter (DzlPreferencesPage *self,
+                                     DzlPatternSpec     *spec);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_PAGE_PRIVATE_H */
diff --git a/src/prefs/dzl-preferences-page.c b/src/prefs/dzl-preferences-page.c
new file mode 100644
index 0000000..d0e15b3
--- /dev/null
+++ b/src/prefs/dzl-preferences-page.c
@@ -0,0 +1,185 @@
+/* dzl-preferences-page.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "prefs/dzl-preferences-group.h"
+#include "prefs/dzl-preferences-group-private.h"
+#include "prefs/dzl-preferences-page.h"
+#include "prefs/dzl-preferences-page-private.h"
+
+enum {
+  PROP_0,
+  PROP_PRIORITY,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (DzlPreferencesPage, dzl_preferences_page, GTK_TYPE_BIN)
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+dzl_preferences_page_finalize (GObject *object)
+{
+  DzlPreferencesPage *self = (DzlPreferencesPage *)object;
+
+  g_clear_pointer (&self->groups_by_name, g_hash_table_unref);
+
+  G_OBJECT_CLASS (dzl_preferences_page_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_page_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  DzlPreferencesPage *self = DZL_PREFERENCES_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PRIORITY:
+      g_value_set_int (value, self->priority);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_page_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  DzlPreferencesPage *self = DZL_PREFERENCES_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PRIORITY:
+      self->priority = g_value_get_int (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_page_class_init (DzlPreferencesPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_page_finalize;
+  object_class->get_property = dzl_preferences_page_get_property;
+  object_class->set_property = dzl_preferences_page_set_property;
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "Priority",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/dazzle/ui/dzl-preferences-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesPage, box);
+}
+
+static void
+dzl_preferences_page_init (DzlPreferencesPage *self)
+{
+  self->groups_by_name = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+dzl_preferences_page_add_group (DzlPreferencesPage  *self,
+                                DzlPreferencesGroup *group)
+{
+  gchar *name = NULL;
+
+  g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self));
+  g_return_if_fail (DZL_IS_PREFERENCES_GROUP (group));
+
+  g_object_get (group, "name", &name, NULL);
+
+  if (g_hash_table_contains (self->groups_by_name, name))
+    {
+      g_free (name);
+      return;
+    }
+
+  g_hash_table_insert (self->groups_by_name, name, group);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->box), GTK_WIDGET (group),
+                                     "priority", dzl_preferences_group_get_priority (group),
+                                     NULL);
+}
+
+/**
+ * dzl_preferences_page_get_group:
+ *
+ * Returns: (transfer none) (nullable): An #DzlPreferencesGroup or %NULL.
+ */
+DzlPreferencesGroup *
+dzl_preferences_page_get_group (DzlPreferencesPage *self,
+                                const gchar        *name)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES_PAGE (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  return g_hash_table_lookup (self->groups_by_name, name);
+}
+
+void
+_dzl_preferences_page_set_map (DzlPreferencesPage *self,
+                               GHashTable         *map)
+{
+  DzlPreferencesGroup *group;
+  GHashTableIter iter;
+
+  g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self));
+
+  g_hash_table_iter_init (&iter, self->groups_by_name);
+
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&group))
+    _dzl_preferences_group_set_map (group, map);
+}
+
+void
+_dzl_preferences_page_refilter (DzlPreferencesPage *self,
+                                DzlPatternSpec     *spec)
+{
+  DzlPreferencesGroup *group;
+  GHashTableIter iter;
+  guint count = 0;
+
+  g_return_if_fail (DZL_IS_PREFERENCES_PAGE (self));
+
+  g_hash_table_iter_init (&iter, self->groups_by_name);
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&group))
+    count += _dzl_preferences_group_refilter (group, spec);
+  gtk_widget_set_visible (GTK_WIDGET (self), count > 0);
+}
diff --git a/src/prefs/dzl-preferences-page.h b/src/prefs/dzl-preferences-page.h
new file mode 100644
index 0000000..515ddd4
--- /dev/null
+++ b/src/prefs/dzl-preferences-page.h
@@ -0,0 +1,39 @@
+/* dzl-preferences-page.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_PAGE_H
+#define DZL_PREFERENCES_PAGE_H
+
+#include <gtk/gtk.h>
+
+#include "prefs/dzl-preferences-group.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_PAGE (dzl_preferences_page_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesPage, dzl_preferences_page, DZL, PREFERENCES_PAGE, GtkBin)
+
+void                 dzl_preferences_page_add_group (DzlPreferencesPage  *self,
+                                                     DzlPreferencesGroup *group);
+DzlPreferencesGroup *dzl_preferences_page_get_group (DzlPreferencesPage  *self,
+                                                     const gchar         *group_name);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_PAGE_H */
diff --git a/src/prefs/dzl-preferences-page.ui b/src/prefs/dzl-preferences-page.ui
new file mode 100644
index 0000000..8f3f2eb
--- /dev/null
+++ b/src/prefs/dzl-preferences-page.ui
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesPage" parent="GtkBin">
+    <child>
+      <object class="DzlPreferencesFlowBox" id="box">
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-spin-button.c b/src/prefs/dzl-preferences-spin-button.c
new file mode 100644
index 0000000..6f2e2ca
--- /dev/null
+++ b/src/prefs/dzl-preferences-spin-button.c
@@ -0,0 +1,417 @@
+/* dzl-preferences-spin-button.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "util/dzl-util-private.h"
+#include "prefs/dzl-preferences-spin-button.h"
+
+struct _DzlPreferencesSpinButton
+{
+  DzlPreferencesBin        parent_instance;
+
+  gulong                   handler;
+
+  guint                    updating : 1;
+
+  gchar                   *key;
+  GSettings               *settings;
+
+  const GVariantType      *type;
+
+  GtkSpinButton           *spin_button;
+  GtkLabel                *title;
+  GtkLabel                *subtitle;
+};
+
+G_DEFINE_TYPE (DzlPreferencesSpinButton, dzl_preferences_spin_button, DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_KEY,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+enum {
+  ACTIVATE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static void
+dzl_preferences_spin_button_activate (DzlPreferencesSpinButton *self)
+{
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->spin_button));
+}
+
+static void
+apply_value (GtkAdjustment *adj,
+             GVariant      *value,
+             const gchar   *property)
+{
+  GValue val = { 0 };
+  gdouble v = 0.0;
+
+  g_assert (GTK_IS_ADJUSTMENT (adj));
+  g_assert (value != NULL);
+  g_assert (property != 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 (G_OBJECT (adj), property, &val);
+  g_value_unset (&val);
+}
+
+static void
+dzl_preferences_spin_button_value_changed (DzlPreferencesSpinButton *self,
+                                           GParamSpec               *pspec,
+                                           GtkSpinButton            *spin_button)
+{
+  GVariant *variant = NULL;
+  gdouble value;
+
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+  g_assert (pspec != NULL);
+  g_assert (GTK_IS_SPIN_BUTTON (spin_button));
+
+  value = gtk_spin_button_get_value (spin_button);
+
+  if (g_variant_type_equal (self->type, G_VARIANT_TYPE_DOUBLE))
+    variant = g_variant_new_double (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT16))
+    variant = g_variant_new_int16 (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT16))
+    variant = g_variant_new_uint16 (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT32))
+    variant = g_variant_new_int32 (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT32))
+    variant = g_variant_new_uint32 (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_INT64))
+    variant = g_variant_new_int64 (value);
+  else if (g_variant_type_equal (self->type, G_VARIANT_TYPE_UINT64))
+    variant = g_variant_new_uint64 (value);
+  else
+    g_return_if_reached ();
+
+  g_variant_ref_sink (variant);
+  g_settings_set_value (self->settings, self->key, variant);
+  g_clear_pointer (&variant, g_variant_unref);
+}
+
+static void
+dzl_preferences_spin_button_setting_changed (DzlPreferencesSpinButton *self,
+                                             const gchar              *key,
+                                             GSettings                *settings)
+{
+  GtkAdjustment *adj;
+  GVariant *value;
+
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+  g_assert (key != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (self->updating)
+    return;
+
+  self->updating = TRUE;
+
+  adj = gtk_spin_button_get_adjustment (self->spin_button);
+
+  value = g_settings_get_value (settings, key);
+  apply_value (adj, value, "value");
+  g_variant_unref (value);
+
+  self->updating = FALSE;
+}
+
+static void
+dzl_preferences_spin_button_connect (DzlPreferencesBin *bin,
+                                     GSettings         *settings)
+{
+  DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin;
+  GSettingsSchema *schema = NULL;
+  GSettingsSchemaKey *key = NULL;
+  GVariant *range = NULL;
+  GVariant *values = NULL;
+  GVariant *lower = NULL;
+  GVariant *upper = NULL;
+  gchar *type = NULL;
+  gchar *signal_detail = NULL;
+  GtkAdjustment *adj;
+  GVariantIter iter;
+
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+
+  self->settings = g_object_ref (settings);
+
+  g_object_get (self->settings, "settings-schema", &schema, NULL);
+
+  adj = gtk_spin_button_get_adjustment (self->spin_button);
+  key = g_settings_schema_get_key (schema, self->key);
+  range = g_settings_schema_key_get_range (key);
+
+  g_variant_get (range, "(sv)", &type, &values);
+
+  if (!dzl_str_equal0 (type, "range") || (2 != g_variant_iter_init (&iter, values)))
+    {
+      gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+      goto cleanup;
+    }
+
+  lower = g_variant_iter_next_value (&iter);
+  upper = g_variant_iter_next_value (&iter);
+
+  self->type = g_variant_get_type (lower);
+
+  apply_value (adj, lower, "lower");
+  apply_value (adj, upper, "upper");
+
+  signal_detail = g_strdup_printf ("changed::%s", self->key);
+
+  self->handler =
+    g_signal_connect_object (self->settings,
+                             signal_detail,
+                             G_CALLBACK (dzl_preferences_spin_button_setting_changed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  dzl_preferences_spin_button_setting_changed (self, self->key, self->settings);
+
+cleanup:
+  g_clear_pointer (&key, g_settings_schema_key_unref);
+  g_clear_pointer (&type, g_free);
+  g_clear_pointer (&signal_detail, g_free);
+  g_clear_pointer (&range, g_variant_unref);
+  g_clear_pointer (&values, g_variant_unref);
+  g_clear_pointer (&lower, g_variant_unref);
+  g_clear_pointer (&upper, g_variant_unref);
+  g_clear_pointer (&schema, g_settings_schema_unref);
+}
+
+static void
+dzl_preferences_spin_button_disconnect (DzlPreferencesBin *bin,
+                                        GSettings         *settings)
+{
+  DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin;
+
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+
+  g_signal_handler_disconnect (settings, self->handler);
+  self->handler = 0;
+}
+
+static gboolean
+dzl_preferences_spin_button_matches (DzlPreferencesBin *bin,
+                                     DzlPatternSpec    *spec)
+{
+  DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)bin;
+  const gchar *tmp;
+
+  g_assert (DZL_IS_PREFERENCES_SPIN_BUTTON (self));
+  g_assert (spec != NULL);
+
+  tmp = gtk_label_get_label (self->title);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  tmp = gtk_label_get_label (self->subtitle);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  if (self->key && dzl_pattern_spec_match (spec, self->key))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+dzl_preferences_spin_button_finalize (GObject *object)
+{
+  DzlPreferencesSpinButton *self = (DzlPreferencesSpinButton *)object;
+
+  g_clear_pointer (&self->key, g_free);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (dzl_preferences_spin_button_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_spin_button_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  DzlPreferencesSpinButton *self = DZL_PREFERENCES_SPIN_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, gtk_label_get_label (self->subtitle));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_spin_button_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  DzlPreferencesSpinButton *self = DZL_PREFERENCES_SPIN_BUTTON (object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+
+    case PROP_SUBTITLE:
+      gtk_label_set_label (self->subtitle, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_spin_button_class_init (DzlPreferencesSpinButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_spin_button_finalize;
+  object_class->get_property = dzl_preferences_spin_button_get_property;
+  object_class->set_property = dzl_preferences_spin_button_set_property;
+
+  bin_class->connect = dzl_preferences_spin_button_connect;
+  bin_class->disconnect = dzl_preferences_spin_button_disconnect;
+  bin_class->matches = dzl_preferences_spin_button_matches;
+
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (dzl_preferences_spin_button_activate),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-spin-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, spin_button);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, subtitle);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSpinButton, title);
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "Key",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "subtitle",
+                         "subtitle",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "title",
+                         "title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+dzl_preferences_spin_button_init (DzlPreferencesSpinButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_object_set (gtk_spin_button_get_adjustment (self->spin_button),
+                "value", 0.0,
+                "lower", 0.0,
+                "upper", 0.0,
+                "step-increment", 1.0,
+                "page-increment", 10.0,
+                "page-size", 10.0,
+                NULL);
+
+  g_signal_connect_object (self->spin_button,
+                           "notify::value",
+                           G_CALLBACK (dzl_preferences_spin_button_value_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+/**
+ * dzl_preferences_spin_button_get_spin_button:
+ *
+ * Returns: (transfer none): The actual spin button widget.
+ */
+GtkWidget *
+dzl_preferences_spin_button_get_spin_button (DzlPreferencesSpinButton *self)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES_SPIN_BUTTON (self), NULL);
+
+  return GTK_WIDGET (self->spin_button);
+}
diff --git a/src/prefs/dzl-preferences-spin-button.h b/src/prefs/dzl-preferences-spin-button.h
new file mode 100644
index 0000000..715e656
--- /dev/null
+++ b/src/prefs/dzl-preferences-spin-button.h
@@ -0,0 +1,36 @@
+/* dzl-preferences-spin-button.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_SPIN_BUTTON_H
+#define DZL_PREFERENCES_SPIN_BUTTON_H
+
+#include <gtk/gtk.h>
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_SPIN_BUTTON (dzl_preferences_spin_button_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesSpinButton, dzl_preferences_spin_button, DZL, PREFERENCES_SPIN_BUTTON, 
DzlPreferencesBin)
+
+GtkWidget *dzl_preferences_spin_button_get_spin_button (DzlPreferencesSpinButton *self);
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_SPIN_BUTTON_H */
diff --git a/src/prefs/dzl-preferences-spin-button.ui b/src/prefs/dzl-preferences-spin-button.ui
new file mode 100644
index 0000000..8b1d92f
--- /dev/null
+++ b/src/prefs/dzl-preferences-spin-button.ui
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesSpinButton" parent="DzlPreferencesBin">
+    <property name="vexpand">false</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel" id="title">
+                <property name="visible">true</property>
+                <property name="vexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="wrap">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="subtitle">
+                <property name="visible">true</property>
+                <property name="vexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="wrap">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.83333"/>
+                </attributes>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSpinButton" id="spin_button">
+            <property name="visible">true</property>
+            <!-- width-chars does not seem to work well (3.19.5) -->
+            <property name="width-request">120</property>
+            <property name="valign">center</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences-switch.c b/src/prefs/dzl-preferences-switch.c
new file mode 100644
index 0000000..907f9c3
--- /dev/null
+++ b/src/prefs/dzl-preferences-switch.c
@@ -0,0 +1,439 @@
+/* dzl-preferences-switch.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "util/dzl-util-private.h"
+#include "prefs/dzl-preferences-switch.h"
+
+struct _DzlPreferencesSwitch
+{
+  DzlPreferencesBin parent_instance;
+
+  guint     is_radio : 1;
+  guint     updating : 1;
+
+  gulong    handler;
+
+  gchar     *key;
+  GVariant  *target;
+  GSettings *settings;
+
+  GtkLabel  *subtitle;
+  GtkLabel  *title;
+  GtkSwitch *widget;
+  GtkImage  *image;
+};
+
+G_DEFINE_TYPE (DzlPreferencesSwitch, dzl_preferences_switch, DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_IS_RADIO,
+  PROP_KEY,
+  PROP_SUBTITLE,
+  PROP_TARGET,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+enum {
+  ACTIVATED,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static void
+dzl_preferences_switch_changed (DzlPreferencesSwitch *self,
+                                const gchar          *key,
+                                GSettings            *settings)
+{
+  GVariant *value;
+  gboolean active = FALSE;
+
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+  g_assert (key != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (self->updating == TRUE)
+    return;
+
+  value = g_settings_get_value (settings, key);
+
+  if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
+    active = g_variant_get_boolean (value);
+  else if ((self->target != NULL) &&
+           g_variant_is_of_type (value, g_variant_get_type (self->target)))
+    active = g_variant_equal (value, self->target);
+  else if ((self->target != NULL) &&
+           g_variant_is_of_type (self->target, G_VARIANT_TYPE_STRING) &&
+           g_variant_is_of_type (value, G_VARIANT_TYPE_STRING_ARRAY))
+    {
+      g_autofree const gchar **strv = g_variant_get_strv (value, NULL);
+      const gchar *flag = g_variant_get_string (self->target, NULL);
+      active = g_strv_contains (strv, flag);
+    }
+
+  self->updating = TRUE;
+
+  if (self->is_radio)
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self->image), active);
+    }
+  else
+    {
+      gtk_switch_set_active (self->widget, active);
+      gtk_switch_set_state (self->widget, active);
+    }
+
+  self->updating = FALSE;
+
+  g_variant_unref (value);
+}
+
+static void
+dzl_preferences_switch_connect (DzlPreferencesBin *bin,
+                                GSettings         *settings)
+{
+  DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin;
+  g_autofree gchar *signal_detail = NULL;
+
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+
+  signal_detail = g_strdup_printf ("changed::%s", self->key);
+
+  self->settings = g_object_ref (settings);
+
+  self->handler =
+    g_signal_connect_object (settings,
+                             signal_detail,
+                             G_CALLBACK (dzl_preferences_switch_changed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  dzl_preferences_switch_changed (self, self->key, settings);
+}
+
+static void
+dzl_preferences_switch_disconnect (DzlPreferencesBin *bin,
+                                   GSettings         *settings)
+{
+  DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin;
+
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+
+  g_signal_handler_disconnect (settings, self->handler);
+  self->handler = 0;
+}
+
+static void
+dzl_preferences_switch_toggle (DzlPreferencesSwitch *self,
+                               gboolean              state)
+{
+  GVariant *value;
+
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+
+  if (self->updating)
+    return;
+
+  self->updating = TRUE;
+
+  value = g_settings_get_value (self->settings, self->key);
+
+  if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN))
+    {
+      g_settings_set_boolean (self->settings, self->key, state);
+    }
+  else if ((self->target != NULL) &&
+           g_variant_is_of_type (self->target, G_VARIANT_TYPE_STRING) &&
+           g_variant_is_of_type (value, G_VARIANT_TYPE_STRING_ARRAY))
+    {
+      g_autofree const gchar **strv = g_variant_get_strv (value, NULL);
+      g_autoptr(GPtrArray) ar = g_ptr_array_new ();
+      const gchar *flag = g_variant_get_string (self->target, NULL);
+      gboolean found = FALSE;
+      gint i;
+
+      for (i = 0; strv [i]; i++)
+        {
+          if (!state && dzl_str_equal0 (strv [i], flag))
+            continue;
+          if (dzl_str_equal0 (strv [i], flag))
+            found = TRUE;
+          g_ptr_array_add (ar, (gchar *)strv [i]);
+        }
+
+      if (state && !found)
+        g_ptr_array_add (ar, (gchar *)flag);
+
+      g_ptr_array_add (ar, NULL);
+
+      g_settings_set_strv (self->settings, self->key, (const gchar * const *)ar->pdata);
+    }
+  else if ((self->target != NULL) &&
+           g_variant_is_of_type (value, g_variant_get_type (self->target)))
+    {
+      g_settings_set_value (self->settings, self->key, self->target);
+    }
+  else
+    {
+      g_warning ("I don't know how to set a variant of type %s to %s",
+                 (const gchar *)g_variant_get_type (value),
+                 self->target ? (const gchar *)g_variant_get_type (self->target) : "(nil)");
+    }
+
+
+  g_variant_unref (value);
+
+  if (self->is_radio)
+    gtk_widget_set_visible (GTK_WIDGET (self->image), state);
+  else
+    gtk_switch_set_state (self->widget, state);
+
+  self->updating = FALSE;
+
+  /* For good measure, so that we cleanup in the boolean deselection case */
+  dzl_preferences_switch_changed (self, self->key, self->settings);
+}
+
+static gboolean
+dzl_preferences_switch_state_set (DzlPreferencesSwitch *self,
+                                  gboolean              state,
+                                  GtkSwitch            *widget)
+{
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+  g_assert (GTK_IS_SWITCH (widget));
+
+  dzl_preferences_switch_toggle (self, state);
+
+  return TRUE;
+}
+
+static void
+dzl_preferences_switch_activate (DzlPreferencesSwitch *self)
+{
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+
+  if (!gtk_widget_get_sensitive (GTK_WIDGET (self)) || (self->settings == NULL))
+    return;
+
+  if (self->is_radio)
+    {
+      gboolean state;
+
+      state = !gtk_widget_get_visible (GTK_WIDGET (self->image));
+      dzl_preferences_switch_toggle (self, state);
+    }
+  else
+    gtk_widget_activate (GTK_WIDGET (self->widget));
+
+}
+
+static gboolean
+dzl_preferences_switch_matches (DzlPreferencesBin *bin,
+                                DzlPatternSpec    *spec)
+{
+  DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)bin;
+  const gchar *tmp;
+
+  g_assert (DZL_IS_PREFERENCES_SWITCH (self));
+  g_assert (spec != NULL);
+
+  tmp = gtk_label_get_label (self->title);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  tmp = gtk_label_get_label (self->subtitle);
+  if (tmp && dzl_pattern_spec_match (spec, tmp))
+    return TRUE;
+
+  if (self->key && dzl_pattern_spec_match (spec, self->key))
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+dzl_preferences_switch_finalize (GObject *object)
+{
+  DzlPreferencesSwitch *self = (DzlPreferencesSwitch *)object;
+
+  g_clear_pointer (&self->key, g_free);
+  g_clear_pointer (&self->target, g_variant_unref);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (dzl_preferences_switch_parent_class)->finalize (object);
+}
+
+static void
+dzl_preferences_switch_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  DzlPreferencesSwitch *self = DZL_PREFERENCES_SWITCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_RADIO:
+      g_value_set_boolean (value, self->is_radio);
+      break;
+
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    case PROP_TARGET:
+      g_value_set_variant (value, self->target);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, gtk_label_get_label (self->subtitle));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_switch_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  DzlPreferencesSwitch *self = DZL_PREFERENCES_SWITCH (object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_RADIO:
+      self->is_radio = g_value_get_boolean (value);
+      gtk_widget_set_visible (GTK_WIDGET (self->widget), !self->is_radio);
+      gtk_widget_set_visible (GTK_WIDGET (self->image), self->is_radio);
+      break;
+
+    case PROP_KEY:
+      self->key = g_value_dup_string (value);
+      break;
+
+    case PROP_TARGET:
+      self->target = g_value_dup_variant (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE:
+      g_object_set (self->subtitle,
+                    "label", g_value_get_string (value),
+                    "visible", !!g_value_get_string (value),
+                    NULL);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+dzl_preferences_switch_class_init (DzlPreferencesSwitchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlPreferencesBinClass *bin_class = DZL_PREFERENCES_BIN_CLASS (klass);
+
+  object_class->finalize = dzl_preferences_switch_finalize;
+  object_class->get_property = dzl_preferences_switch_get_property;
+  object_class->set_property = dzl_preferences_switch_set_property;
+
+  bin_class->connect = dzl_preferences_switch_connect;
+  bin_class->disconnect = dzl_preferences_switch_disconnect;
+  bin_class->matches = dzl_preferences_switch_matches;
+
+  signals [ACTIVATED] =
+    g_signal_new_class_handler ("activated",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (dzl_preferences_switch_activate),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  widget_class->activate_signal = signals [ACTIVATED];
+
+  properties [PROP_IS_RADIO] =
+    g_param_spec_boolean ("is-radio",
+                         "Is Radio",
+                         "If a radio style should be used instead of a switch.",
+                         FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+
+  properties [PROP_TARGET] =
+    g_param_spec_variant ("target",
+                          "Target",
+                          "Target",
+                          G_VARIANT_TYPE_ANY,
+                          NULL,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "Key",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "Subtitle",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/dazzle/ui/dzl-preferences-switch.ui");
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, image);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, subtitle);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, title);
+  gtk_widget_class_bind_template_child (widget_class, DzlPreferencesSwitch, widget);
+}
+
+static void
+dzl_preferences_switch_init (DzlPreferencesSwitch *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->widget,
+                           "state-set",
+                           G_CALLBACK (dzl_preferences_switch_state_set),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/prefs/dzl-preferences-switch.h b/src/prefs/dzl-preferences-switch.h
new file mode 100644
index 0000000..7f83523
--- /dev/null
+++ b/src/prefs/dzl-preferences-switch.h
@@ -0,0 +1,32 @@
+/* dzl-preferences-switch.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_SWITCH_H
+#define DZL_PREFERENCES_SWITCH_H
+
+#include "prefs/dzl-preferences-bin.h"
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES_SWITCH (dzl_preferences_switch_get_type())
+
+G_DECLARE_FINAL_TYPE (DzlPreferencesSwitch, dzl_preferences_switch, DZL, PREFERENCES_SWITCH, 
DzlPreferencesBin)
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_SWITCH_H */
diff --git a/src/prefs/dzl-preferences-switch.ui b/src/prefs/dzl-preferences-switch.ui
new file mode 100644
index 0000000..e11b0b0
--- /dev/null
+++ b/src/prefs/dzl-preferences-switch.ui
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="DzlPreferencesSwitch" parent="DzlPreferencesBin">
+    <property name="vexpand">false</property>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel" id="title">
+                <property name="visible">true</property>
+                <property name="vexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="wrap">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="subtitle">
+                <property name="vexpand">true</property>
+                <property name="xalign">0.0</property>
+                <property name="wrap">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.83333"/>
+                </attributes>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkImage" id="image">
+            <property name="icon-name">object-select-symbolic</property>
+            <property name="valign">center</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="hexpand">true</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSwitch" id="widget">
+            <property name="visible">true</property>
+            <property name="valign">center</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/prefs/dzl-preferences.c b/src/prefs/dzl-preferences.c
new file mode 100644
index 0000000..c791217
--- /dev/null
+++ b/src/prefs/dzl-preferences.c
@@ -0,0 +1,260 @@
+/* dzl-preferences.c
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "prefs/dzl-preferences.h"
+
+G_DEFINE_INTERFACE (DzlPreferences, dzl_preferences, G_TYPE_OBJECT)
+
+static void
+dzl_preferences_default_init (DzlPreferencesInterface *iface)
+{
+}
+
+void
+dzl_preferences_add_page (DzlPreferences *self,
+                          const gchar    *page_name,
+                          const gchar    *title,
+                          gint            priority)
+{
+  g_return_if_fail (DZL_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+  g_return_if_fail ((title != NULL) || (strchr (page_name, '.') != NULL));
+
+  DZL_PREFERENCES_GET_IFACE (self)->add_page (self, page_name, title, priority);
+}
+
+void
+dzl_preferences_add_group (DzlPreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *title,
+                           gint            priority)
+{
+  g_return_if_fail (DZL_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+  g_return_if_fail (group_name != NULL);
+
+  DZL_PREFERENCES_GET_IFACE (self)->add_group (self, page_name, group_name, title, priority);
+}
+
+/**
+ * dzl_preferences_add_switch:
+ * @path: (nullable): An optional path
+ * @variant_string: (nullable): An optional gvariant string
+ * @title: (nullable): An optional title
+ * @subtitle: (nullable): An optional subtitle
+ * @keywords: (nullable): Optional keywords for search
+ *
+ */
+guint
+dzl_preferences_add_switch (DzlPreferences *self,
+                            const gchar    *page_name,
+                            const gchar    *group_name,
+                            const gchar    *schema_id,
+                            const gchar    *key,
+                            const gchar    *path,
+                            const gchar    *variant_string,
+                            const gchar    *title,
+                            const gchar    *subtitle,
+                            const gchar    *keywords,
+                            gint            priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_switch (self, page_name, group_name, schema_id, key, path, 
variant_string, title, subtitle, keywords, priority);
+}
+
+guint
+dzl_preferences_add_spin_button (DzlPreferences *self,
+                                 const gchar    *page_name,
+                                 const gchar    *group_name,
+                                 const gchar    *schema_id,
+                                 const gchar    *key,
+                                 const gchar    *path,
+                                 const gchar    *title,
+                                 const gchar    *subtitle,
+                                 const gchar    *keywords,
+                                 gint            priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_spin_button (self, page_name, group_name, schema_id, key, 
path, title, subtitle, keywords, priority);
+}
+
+/**
+ * dzl_preferences_add_custom:
+ * @keywords: (nullable): Optional keywords for search
+ *
+ */
+guint
+dzl_preferences_add_custom (DzlPreferences *self,
+                            const gchar    *page_name,
+                            const gchar    *group_name,
+                            GtkWidget      *widget,
+                            const gchar    *keywords,
+                            gint            priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_custom (self, page_name, group_name, widget, keywords, 
priority);
+}
+
+void
+dzl_preferences_add_list_group (DzlPreferences   *self,
+                                const gchar      *page_name,
+                                const gchar      *group_name,
+                                const gchar      *title,
+                                GtkSelectionMode  mode,
+                                gint              priority)
+{
+  g_return_if_fail (DZL_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+  g_return_if_fail (group_name != NULL);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_list_group  (self, page_name, group_name, title, mode, 
priority);
+}
+
+/**
+ * dzl_preferences_add_radio:
+ * @path: (nullable): An optional path
+ * @variant_string: (nullable): An optional gvariant string
+ * @title: (nullable): An optional title
+ * @subtitle: (nullable): An optional subtitle
+ * @keywords: (nullable): Optional keywords for search
+ *
+ */
+guint
+dzl_preferences_add_radio (DzlPreferences *self,
+                           const gchar    *page_name,
+                           const gchar    *group_name,
+                           const gchar    *schema_id,
+                           const gchar    *key,
+                           const gchar    *path,
+                           const gchar    *variant_string,
+                           const gchar    *title,
+                           const gchar    *subtitle,
+                           const gchar    *keywords,
+                           gint            priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_radio (self, page_name, group_name, schema_id, key, path, 
variant_string, title, subtitle, keywords, priority);
+}
+
+guint
+dzl_preferences_add_font_button (DzlPreferences *self,
+                                 const gchar    *page_name,
+                                 const gchar    *group_name,
+                                 const gchar    *schema_id,
+                                 const gchar    *key,
+                                 const gchar    *title,
+                                 const gchar    *keywords,
+                                 gint            priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_font_button (self, page_name, group_name, schema_id, key, 
title, keywords, priority);
+}
+
+guint
+dzl_preferences_add_file_chooser (DzlPreferences      *self,
+                                  const gchar         *page_name,
+                                  const gchar         *group_name,
+                                  const gchar         *schema_id,
+                                  const gchar         *key,
+                                  const gchar         *path,
+                                  const gchar         *title,
+                                  const gchar         *subtitle,
+                                  GtkFileChooserAction action,
+                                  const gchar         *keywords,
+                                  gint                 priority)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), 0);
+  g_return_val_if_fail (page_name != NULL, 0);
+  g_return_val_if_fail (group_name != NULL, 0);
+  g_return_val_if_fail (schema_id != NULL, 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (title != NULL, 0);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->add_file_chooser (self, page_name, group_name, schema_id, key, 
path, title, subtitle, action, keywords, priority);
+}
+
+/**
+ * ide_preference_remove_id:
+ * @widget_id: An preferences widget id
+ *
+ */
+gboolean
+dzl_preferences_remove_id (DzlPreferences *self,
+                           guint           widget_id)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), FALSE);
+  g_return_val_if_fail (widget_id, FALSE);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->remove_id (self, widget_id);
+}
+
+void
+dzl_preferences_set_page (DzlPreferences *self,
+                          const gchar    *page_name,
+                          GHashTable     *map)
+{
+  g_return_if_fail (DZL_IS_PREFERENCES (self));
+  g_return_if_fail (page_name != NULL);
+
+  DZL_PREFERENCES_GET_IFACE (self)->set_page (self, page_name, map);
+}
+
+/**
+ * dzl_preferences_get_widget:
+ *
+ * Returns: (transfer none) (nullable): A #GtkWidget or %NULL.
+ */
+GtkWidget *
+dzl_preferences_get_widget (DzlPreferences *self,
+                            guint           widget_id)
+{
+  g_return_val_if_fail (DZL_IS_PREFERENCES (self), NULL);
+
+  return DZL_PREFERENCES_GET_IFACE (self)->get_widget (self, widget_id);
+}
diff --git a/src/prefs/dzl-preferences.h b/src/prefs/dzl-preferences.h
new file mode 100644
index 0000000..31748cd
--- /dev/null
+++ b/src/prefs/dzl-preferences.h
@@ -0,0 +1,200 @@
+/* dzl-preferences.h
+ *
+ * Copyright (C) 2015-2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef DZL_PREFERENCES_H
+#define DZL_PREFERENCES_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define DZL_TYPE_PREFERENCES (dzl_preferences_get_type())
+
+G_DECLARE_INTERFACE (DzlPreferences, dzl_preferences, DZL, PREFERENCES, GObject)
+
+struct _DzlPreferencesInterface
+{
+  GTypeInterface parent_interface;
+
+  void     (*set_page)         (DzlPreferences *self,
+                                const gchar    *page_name,
+                                GHashTable     *map);
+  void     (*add_page)         (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *title,
+                                gint            priority);
+  void     (*add_group)        (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *title,
+                                gint            priority);
+  void     (*add_list_group)   (DzlPreferences   *self,
+                                const gchar      *page_name,
+                                const gchar      *group_name,
+                                const gchar      *title,
+                                GtkSelectionMode  mode,
+                                gint              priority);
+  guint    (*add_radio)        (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *schema_id,
+                                const gchar    *key,
+                                const gchar    *path,
+                                const gchar    *variant_string,
+                                const gchar    *title,
+                                const gchar    *subtitle,
+                                const gchar    *keywords,
+                                gint            priority);
+  guint    (*add_font_button)  (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *schema_id,
+                                const gchar    *key,
+                                const gchar    *title,
+                                const gchar    *keywords,
+                                gint            priority);
+  guint    (*add_switch)       (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *schema_id,
+                                const gchar    *key,
+                                const gchar    *path,
+                                const gchar    *variant_string,
+                                const gchar    *title,
+                                const gchar    *subtitle,
+                                const gchar    *keywords,
+                                gint            priority);
+  guint    (*add_spin_button)  (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                const gchar    *schema_id,
+                                const gchar    *key,
+                                const gchar    *path,
+                                const gchar    *title,
+                                const gchar    *subtitle,
+                                const gchar    *keywords,
+                                gint            priority);
+  guint    (*add_file_chooser) (DzlPreferences      *self,
+                                const gchar         *page_name,
+                                const gchar         *group_name,
+                                const gchar         *schema_id,
+                                const gchar         *key,
+                                const gchar         *path,
+                                const gchar         *title,
+                                const gchar         *subtitle,
+                                GtkFileChooserAction action,
+                                const gchar         *keywords,
+                                gint                 priority);
+  guint    (*add_custom)       (DzlPreferences *self,
+                                const gchar    *page_name,
+                                const gchar    *group_name,
+                                GtkWidget      *widget,
+                                const gchar    *keywords,
+                                gint            priority);
+
+  gboolean (*remove_id)        (DzlPreferences *self,
+                                guint           widget_id);
+
+  GtkWidget *(*get_widget)     (DzlPreferences *self,
+                                guint           widget_id);
+};
+
+void     dzl_preferences_add_page         (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *title,
+                                           gint            priority);
+void     dzl_preferences_add_group        (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           const gchar    *title,
+                                           gint            priority);
+void     dzl_preferences_add_list_group   (DzlPreferences   *self,
+                                           const gchar      *page_name,
+                                           const gchar      *group_name,
+                                           const gchar      *title,
+                                           GtkSelectionMode  mode,
+                                           gint              priority);
+guint    dzl_preferences_add_radio        (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           const gchar    *schema_id,
+                                           const gchar    *key,
+                                           const gchar    *path,
+                                           const gchar    *variant_string,
+                                           const gchar    *title,
+                                           const gchar    *subtitle,
+                                           const gchar    *keywords,
+                                           gint            priority);
+guint    dzl_preferences_add_switch       (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           const gchar    *schema_id,
+                                           const gchar    *key,
+                                           const gchar    *path,
+                                           const gchar    *variant_string,
+                                           const gchar    *title,
+                                           const gchar    *subtitle,
+                                           const gchar    *keywords,
+                                           gint            priority);
+guint    dzl_preferences_add_spin_button  (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           const gchar    *schema_id,
+                                           const gchar    *key,
+                                           const gchar    *path,
+                                           const gchar    *title,
+                                           const gchar    *subtitle,
+                                           const gchar    *keywords,
+                                           gint            priority);
+guint    dzl_preferences_add_custom       (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           GtkWidget      *widget,
+                                           const gchar    *keywords,
+                                           gint            priority);
+guint    dzl_preferences_add_font_button  (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           const gchar    *group_name,
+                                           const gchar    *schema_id,
+                                           const gchar    *key,
+                                           const gchar    *title,
+                                           const gchar    *keywords,
+                                           gint            priority);
+guint    dzl_preferences_add_file_chooser (DzlPreferences      *self,
+                                           const gchar         *page_name,
+                                           const gchar         *group_name,
+                                           const gchar         *schema_id,
+                                           const gchar         *key,
+                                           const gchar         *path,
+                                           const gchar         *title,
+                                           const gchar         *subtitle,
+                                           GtkFileChooserAction action,
+                                           const gchar         *keywords,
+                                           gint                 priority);
+gboolean dzl_preferences_remove_id        (DzlPreferences *self,
+                                           guint           widget_id);
+void     dzl_preferences_set_page         (DzlPreferences *self,
+                                           const gchar    *page_name,
+                                           GHashTable     *map);
+GtkWidget *dzl_preferences_get_widget     (DzlPreferences *self,
+                                           guint           widget_id);
+
+
+G_END_DECLS
+
+#endif /* DZL_PREFERENCES_H */


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