[gnome-builder/wip/chergert/shortcuts] shortcuts: start on cleaner shortcuts abstraction



commit d6ea561510ed63e90ce6d2fa7407466cc531bd04
Author: Christian Hergert <christian hergert me>
Date:   Sun Aug 30 18:38:36 2015 -0700

    shortcuts: start on cleaner shortcuts abstraction

 data/ui/gb-shortcuts-window.ui        |  137 +++------
 src/Makefile.am                       |   15 +
 src/keybindings/gb-shortcuts-window.c |  537 +--------------------------------
 src/keybindings/gb-shortcuts-window.h |    6 +-
 src/shortcuts/gb-shortcuts-column.c   |   38 +++
 src/shortcuts/gb-shortcuts-column.h   |   32 ++
 src/shortcuts/gb-shortcuts-dialog.c   |  273 +++++++++++++++++
 src/shortcuts/gb-shortcuts-dialog.h   |   39 +++
 src/shortcuts/gb-shortcuts-gesture.c  |  194 ++++++++++++
 src/shortcuts/gb-shortcuts-gesture.h  |   32 ++
 src/shortcuts/gb-shortcuts-group.c    |  111 +++++++
 src/shortcuts/gb-shortcuts-group.h    |   32 ++
 src/shortcuts/gb-shortcuts-page.c     |   37 +++
 src/shortcuts/gb-shortcuts-page.h     |   32 ++
 src/shortcuts/gb-shortcuts-shortcut.c |  139 +++++++++
 src/shortcuts/gb-shortcuts-shortcut.h |   32 ++
 src/shortcuts/gb-shortcuts-view.c     |  215 +++++++++++++
 src/shortcuts/gb-shortcuts-view.h     |   35 +++
 18 files changed, 1307 insertions(+), 629 deletions(-)
---
diff --git a/data/ui/gb-shortcuts-window.ui b/data/ui/gb-shortcuts-window.ui
index 062c3f7..f893dce 100644
--- a/data/ui/gb-shortcuts-window.ui
+++ b/data/ui/gb-shortcuts-window.ui
@@ -1,115 +1,72 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.16 -->
-  <template class="GbShortcutsWindow" parent="GtkWindow">
-    <property name="resizable">false</property>
-    <child type="titlebar">
-      <object class="GtkHeaderBar">
-        <property name="show-close-button">true</property>
+  <!-- interface-requires gtk+ 3.17 -->
+  <template class="GbShortcutsWindow" parent="GbShortcutsDialog">
+    <child>
+      <object class="GbShortcutsView">
         <property name="visible">true</property>
-        <child type="title">
-          <object class="GtkMenuButton" id="menu_button">
-            <property name="focus-on-click">false</property>
-            <property name="popover">popover</property>
+        <property name="view-name">editor</property>
+        <property name="title" translatable="yes">Editor Shortcuts</property>
+        <child>
+          <object class="GbShortcutsPage">
             <property name="visible">true</property>
-            <style>
-              <class name="flat"/>
-            </style>
             <child>
-              <object class="GtkBox">
-                <property name="orientation">horizontal</property>
-                <property name="spacing">6</property>
+              <object class="GbShortcutsColumn">
                 <property name="visible">true</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
                 <child>
-                  <object class="GtkArrow">
-                    <property name="arrow-type">down</property>
+                  <object class="GbShortcutsGroup">
+                    <property name="title" translatable="yes">General</property>
                     <property name="visible">true</property>
+                    <child>
+                      <object class="GbShortcutsShortcut">
+                        <property name="accelerator">&lt;ctrl&gt;period</property>
+                        <property name="title" translatable="yes">Global Search</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GbShortcutsShortcut">
+                        <property name="accelerator">&lt;ctrl&gt;comma</property>
+                        <property name="title" translatable="yes">Preferences</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
                   </object>
-                  <packing>
-                    <property name="pack-type">end</property>
-                  </packing>
                 </child>
-                <child type="center">
-                  <!-- FIXME: hrmm, child="center" doesn't seem to be working -->
-                  <object class="GtkLabel" id="menu_label">
+                <child>
+                  <object class="GbShortcutsGroup">
+                    <property name="title" translatable="yes">Touchpad gestures</property>
                     <property name="visible">true</property>
+                    <child>
+                      <object class="GbShortcutsGesture">
+                        <property name="icon-name">gesture-two-finger-swipe-right</property>
+                        <property name="title" translatable="yes">Switch to the next document</property>
+                        <property name="subtitle" translatable="yes">Two finger swipe right</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GbShortcutsGesture">
+                        <property name="icon-name">gesture-two-finger-swipe-left</property>
+                        <property name="title" translatable="yes">Switch to the previous document</property>
+                        <property name="subtitle" translatable="yes">Two finger swipe left</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
             </child>
           </object>
         </child>
-        <child>
-          <object class="GtkToggleButton">
-            <property name="active" bind-source="search_bar" bind-property="search-mode-enabled" 
bind-flags="bidirectional">false</property>
-            <property name="visible">true</property>
-            <style>
-              <class name="image-button"/>
-            </style>
-            <child>
-              <object class="GtkImage">
-                <property name="icon-name">edit-find-symbolic</property>
-                <property name="visible">true</property>
-              </object>
-            </child>
-          </object>
-        </child>
       </object>
     </child>
     <child>
-      <object class="GtkBox">
-        <property name="orientation">vertical</property>
+      <object class="GbShortcutsView">
         <property name="visible">true</property>
-        <child>
-          <object class="EggSearchBar" id="search_bar">
-            <property name="visible">true</property>
-            <child internal-child="entry">
-              <object class="GtkSearchEntry" id="search_entry">
-                <property name="placeholder-text" translatable="yes">Search keyboard shortcuts</property>
-                <property name="width-chars">50</property>
-              </object>
-            </child>
-          </object>
-        </child>
-        <child>
-          <object class="GtkStack" id="stack">
-            <property name="vexpand">true</property>
-            <property name="visible">true</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkStackSwitcher" id="switcher">
-            <property name="visible">true</property>
-            <style>
-              <class name="round"/>
-            </style>
-          </object>
-        </child>
+        <property name="view-name">terminal</property>
+        <property name="title" translatable="yes">Terminal Shortcuts</property>
       </object>
     </child>
   </template>
-  <object class="GtkPopover" id="popover">
-    <child>
-      <object class="GbScrolledWindow">
-        <property name="max-content-height">300</property>
-        <property name="max-content-width">300</property>
-        <property name="min-content-width">150</property>
-        <property name="border-width">6</property>
-        <property name="shadow-type">in</property>
-        <property name="visible">true</property>
-        <child>
-          <object class="GtkListBox" id="list_box">
-            <property name="visible">true</property>
-            <property name="selection-mode">none</property>
-            <style>
-              <class name="shortcuts-list-box"/>
-            </style>
-          </object>
-        </child>
-      </object>
-    </child>
-  </object>
 </interface>
diff --git a/src/Makefile.am b/src/Makefile.am
index d35b785..ee455e9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -97,6 +97,20 @@ libgnome_builder_la_SOURCES = \
        project-tree/gb-rename-file-popover.h \
        scrolledwindow/gb-scrolled-window.c \
        scrolledwindow/gb-scrolled-window.h \
+       shortcuts/gb-shortcuts-column.c \
+       shortcuts/gb-shortcuts-column.h \
+       shortcuts/gb-shortcuts-dialog.c \
+       shortcuts/gb-shortcuts-dialog.h \
+       shortcuts/gb-shortcuts-gesture.c \
+       shortcuts/gb-shortcuts-gesture.h \
+       shortcuts/gb-shortcuts-group.c \
+       shortcuts/gb-shortcuts-group.h \
+       shortcuts/gb-shortcuts-page.c \
+       shortcuts/gb-shortcuts-page.h \
+       shortcuts/gb-shortcuts-shortcut.c \
+       shortcuts/gb-shortcuts-shortcut.h \
+       shortcuts/gb-shortcuts-view.c \
+       shortcuts/gb-shortcuts-view.h \
        search/gb-search-box.c \
        search/gb-search-box.h \
        search/gb-search-display-group.c \
@@ -215,6 +229,7 @@ libgnome_builder_la_CFLAGS = \
        -I$(srcdir)/resources \
        -I$(srcdir)/scrolledwindow \
        -I$(srcdir)/search \
+       -I$(srcdir)/shortcuts \
        -I$(srcdir)/support \
        -I$(srcdir)/tree \
        -I$(srcdir)/util \
diff --git a/src/keybindings/gb-shortcuts-window.c b/src/keybindings/gb-shortcuts-window.c
index 08ccf9a..d9ff08b 100644
--- a/src/keybindings/gb-shortcuts-window.c
+++ b/src/keybindings/gb-shortcuts-window.c
@@ -16,558 +16,25 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-/*
- * NOTE:
- *
- * Thar be dragons!
- *
- * This code uses XMACROS. It's a bit insane, and does lots of
- * duplication that could be cleaned up next cycle.
- *
- * If you want this code for your module, I suggest you refactor
- * this to use GtkBuilder with custom_tag_start().
- *
- * I'd like to see something like:
- *
- *  <views>
- *    <view>
- *      <page>
- *        <column>
- *          <group>
- *            <shortcut />
- *            <shortcut />
- *            <gesture />
- *
- * Or whatever else you come up with and are willing to maintain.
- *
- * -- Christian
- */
-
-#include <glib/gi18n.h>
-#include <ide.h>
-
-#include "egg-search-bar.h"
-
-#include "gb-accel-label.h"
-#include "gb-scrolled-window.h"
 #include "gb-shortcuts-window.h"
-#include "gb-widget.h"
 
 struct _GbShortcutsWindow
 {
-  GtkWindow       parent_instance;
-
-  GHashTable     *widget_keywords;
-
-  GtkListBox     *list_box;
-  GtkStack       *stack;
-  GtkMenuButton  *menu_button;
-  GtkLabel       *menu_label;
-  GtkPopover     *popover;
-  EggSearchBar   *search_bar;
-  GtkSearchEntry *search_entry;
-
-  GtkWidget      *previous_view;
+  GbShortcutsDialog parent_instance;
 };
 
-G_DEFINE_TYPE (GbShortcutsWindow, gb_shortcuts_window, GTK_TYPE_WINDOW)
-
-enum {
-  CLOSE,
-  LAST_SIGNAL
-};
-
-static guint gSignals [LAST_SIGNAL];
-
-GtkWidget *
-gb_shortcuts_window_new (void)
-{
-  return g_object_new (GB_TYPE_SHORTCUTS_WINDOW, NULL);
-}
-
-static void
-gb_shortcuts_window_set_view (GbShortcutsWindow *self,
-                              const gchar       *name)
-{
-  GtkWidget *child;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-
-  child = gtk_stack_get_child_by_name (self->stack, name);
-
-  if (child != NULL)
-    {
-      g_autofree gchar *title = NULL;
-
-      gtk_container_child_get (GTK_CONTAINER (self->stack), child,
-                               "title", &title,
-                               NULL);
-      gtk_label_set_label (self->menu_label, title);
-      gtk_stack_set_visible_child (self->stack, child);
-    }
-}
-
-static void
-register_keywords (GbShortcutsWindow *self,
-                   GtkWidget         *widget,
-                   const gchar       *keywords)
-{
-  gchar *casefold;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-
-  casefold = g_utf8_casefold (keywords, -1);
-  g_hash_table_insert (self->widget_keywords, widget, casefold);
-}
-
-static void
-adjust_page_buttons (GtkWidget *widget,
-                     gpointer   data)
-{
-  /*
-   * TODO: This is a hack to get the GtkStackSwitcher radio
-   *       buttons to look how we want. However, it's very
-   *       much font size specific.
-   */
-  gtk_widget_set_size_request (widget, 34, 34);
-}
-
-static void
-gb_shortcuts_window_build (GbShortcutsWindow *self)
-{
-  GtkSizeGroup *desc_group = NULL;
-  gboolean do_keywords = FALSE;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-
-#define VIEWS(_views) { _views }
-#define VIEW(_ident, _name, _pages) \
-  { \
-    GtkBox *view; \
-    GtkStack *stack; \
-    GtkStackSwitcher *switcher; \
-    guint page_count = 0; \
-    view = g_object_new (GTK_TYPE_BOX, \
-                         "border-width", 24, \
-                         "spacing", 22, \
-                         "orientation", GTK_ORIENTATION_VERTICAL, \
-                         "visible", TRUE, \
-                         "vexpand", TRUE, \
-                         NULL); \
-    stack = g_object_new (GTK_TYPE_STACK, \
-                          "visible", TRUE, \
-                          "vexpand", TRUE, \
-                          "transition-type", GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT, \
-                          "homogeneous", TRUE, \
-                          NULL); \
-    gtk_container_add (GTK_CONTAINER (view), GTK_WIDGET (stack)); \
-    switcher = g_object_new (GTK_TYPE_STACK_SWITCHER, \
-                             "halign", GTK_ALIGN_CENTER, \
-                             "spacing", 12, \
-                             "stack", stack, \
-                             NULL); \
-    gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (switcher)), "round"); \
-    gtk_container_add (GTK_CONTAINER (view), GTK_WIDGET (switcher)); \
-    gtk_stack_add_titled (self->stack, GTK_WIDGET (view), _ident, _name); \
-    _pages \
-    if (page_count > 1) \
-      gtk_widget_show (GTK_WIDGET (switcher)); \
-    gtk_style_context_remove_class (gtk_widget_get_style_context (GTK_WIDGET (switcher)), "linked"); \
-    gtk_container_foreach (GTK_CONTAINER (switcher), adjust_page_buttons, NULL); \
-    { \
-      GtkListBoxRow *row; \
-      row = g_object_new (GTK_TYPE_LIST_BOX_ROW, \
-                          "visible", TRUE, \
-                          "child", g_object_new (GTK_TYPE_LABEL, \
-                                                 "label", _name, \
-                                                 "visible", TRUE, \
-                                                 "xalign", 0.5f, \
-                                                 NULL), \
-                          NULL); \
-      g_object_set_data (G_OBJECT (row), "view", _ident); \
-      gtk_container_add (GTK_CONTAINER (self->list_box), GTK_WIDGET (row)); \
-    } \
-  }
-#define PAGE(_columns) \
-  { \
-    g_autofree gchar *title = g_strdup_printf ("%u", ++page_count); \
-    GtkBox *page; \
-    page = g_object_new (GTK_TYPE_BOX, \
-                         "homogeneous", FALSE, \
-                         "orientation", GTK_ORIENTATION_HORIZONTAL, \
-                         "spacing", 24, \
-                         "visible", TRUE, \
-                         NULL); \
-    _columns \
-    gtk_stack_add_titled (stack, GTK_WIDGET (page), title, title); \
-  }
-#define COLUMN(_groups) \
-  { \
-    GtkBox *column; \
-    GtkSizeGroup *size_group; \
-    GtkSizeGroup *mod_key_group; \
-    column = g_object_new (GTK_TYPE_BOX, \
-                           "orientation", GTK_ORIENTATION_VERTICAL, \
-                           "spacing", 22, \
-                           "visible", TRUE, \
-                           NULL); \
-    gtk_container_add (GTK_CONTAINER (page), GTK_WIDGET (column)); \
-    size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); \
-    mod_key_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL); \
-    _groups \
-    g_clear_object (&size_group); \
-    g_clear_object (&mod_key_group); \
-  }
-#define GROUP(_group_name, _shortcuts) \
-  { \
-    g_autofree gchar *title_markup = g_strdup_printf ("<b>%s</b>", _group_name); \
-    GtkBox *group; \
-    GtkBox *title_label; \
-    group = g_object_new (GTK_TYPE_BOX, \
-                          "orientation", GTK_ORIENTATION_VERTICAL, \
-                          "spacing", 10, \
-                          "visible", TRUE, \
-                          NULL); \
-    gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (group)); \
-    title_label = g_object_new (GTK_TYPE_LABEL, \
-                                "visible", TRUE, \
-                                "label", title_markup, \
-                                "use-markup", TRUE, \
-                                "xalign", 0.0f, \
-                                NULL); \
-    gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (title_label)); \
-    _shortcuts \
-  }
-#define SHORTCUT(_accel, _desc) \
-  { \
-    g_autofree gchar *keywords = NULL; \
-    GtkBox *shortcut; \
-    GbAccelLabel *accel; \
-    GtkLabel *desc; \
-    shortcut = g_object_new (GTK_TYPE_BOX, \
-                             "orientation", GTK_ORIENTATION_HORIZONTAL, \
-                             "spacing", 10, \
-                             "visible", TRUE, \
-                             NULL); \
-    gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (shortcut)); \
-    accel = g_object_new (GB_TYPE_ACCEL_LABEL, \
-                          "accelerator", _accel, \
-                          "size-group", mod_key_group, \
-                          "halign", GTK_ALIGN_START, \
-                          "visible", TRUE, \
-                          NULL); \
-    gtk_size_group_add_widget (size_group, GTK_WIDGET (accel)); \
-    gtk_container_add (GTK_CONTAINER (shortcut), GTK_WIDGET (accel)); \
-    desc = g_object_new (GTK_TYPE_LABEL, \
-                         "label", _desc, \
-                         "visible", TRUE, \
-                         "xalign", 0.0f, \
-                         "hexpand", TRUE, \
-                         NULL); \
-    gtk_container_add (GTK_CONTAINER (shortcut), GTK_WIDGET (desc)); \
-    if (desc_group != NULL) \
-      gtk_size_group_add_widget (desc_group, GTK_WIDGET (desc)); \
-    if (do_keywords) \
-      { \
-        keywords = g_strdup_printf ("%s %s", _accel, _desc); \
-        register_keywords (self, GTK_WIDGET (shortcut), keywords); \
-      } \
-  }
-#define GESTURE(_accel, _title, _subtitle) \
-  { \
-    g_autofree gchar *keywords = NULL; \
-    GtkBox *gesture; \
-    GtkLabel *primary; \
-    GtkLabel *subtitle; \
-    GtkImage *image; \
-    gesture = g_object_new (GTK_TYPE_GRID, \
-                            "column-spacing", 12, \
-                            "visible", TRUE, \
-                            NULL); \
-    gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (gesture)); \
-    image = g_object_new (GTK_TYPE_IMAGE, \
-                          "resource", "/org/gnome/builder/icons/scalable/actions/gesture-"_accel".svg", \
-                          "xalign", 0.0f, \
-                          "valign", GTK_ALIGN_CENTER, \
-                          "visible", TRUE, \
-                          NULL); \
-    gtk_container_add_with_properties (GTK_CONTAINER (gesture), GTK_WIDGET (image), \
-                                       "height", 2, \
-                                       NULL); \
-    primary = g_object_new (GTK_TYPE_LABEL, \
-                            "label", _title, \
-                            "visible", TRUE, \
-                            "valign", GTK_ALIGN_END, \
-                            "xalign", 0.0f, \
-                            "hexpand", TRUE, \
-                            NULL); \
-    gtk_container_add_with_properties (GTK_CONTAINER (gesture), GTK_WIDGET (primary), \
-                                       "left-attach", 1, \
-                                       NULL); \
-    subtitle = g_object_new (GTK_TYPE_LABEL, \
-                             "label", _subtitle, \
-                             "visible", TRUE, \
-                             "valign", GTK_ALIGN_START, \
-                             "xalign", 0.0f, \
-                             "hexpand", TRUE, \
-                             NULL); \
-    if (desc_group != NULL) \
-      gtk_size_group_add_widget (desc_group, GTK_WIDGET (subtitle)); \
-    gb_widget_add_style_class (GTK_WIDGET (subtitle), "dim-label"); \
-    gtk_container_add_with_properties (GTK_CONTAINER (gesture), GTK_WIDGET (subtitle), \
-                                       "top-attach", 1, \
-                                       "left-attach", 1, \
-                                       NULL); \
-    if (do_keywords) \
-      { \
-        keywords = g_strdup_printf ("%s %s %s", _accel, _title, _subtitle); \
-        register_keywords (self, GTK_WIDGET (gesture), keywords); \
-        gtk_size_group_add_widget (size_group, GTK_WIDGET (image)); \
-      } \
-  }
-
-#include "gb-shortcuts-window.defs"
-
-#undef VIEWS
-#undef VIEW
-#undef PAGE
-#undef COLUMN
-#undef GROUP
-
-  /*
-   * Now add the page where we show search results.
-   */
-  {
-    GtkScrolledWindow *page;
-    GtkSizeGroup *mod_key_group;
-    GtkSizeGroup *size_group;
-    GtkBox *group;
-
-    page = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
-                         "visible", TRUE,
-                         NULL);
-    gtk_stack_add_titled (self->stack,
-                          GTK_WIDGET (page),
-                          "internal-search",
-                          _("Search Results"));
-
-    group = g_object_new (GTK_TYPE_BOX,
-                          "border-width", 24,
-                          "halign", GTK_ALIGN_CENTER,
-                          "orientation", GTK_ORIENTATION_VERTICAL,
-                          "spacing", 12,
-                          "visible", TRUE,
-                          NULL);
-    gtk_container_add (GTK_CONTAINER (page), GTK_WIDGET (group));
-
-    mod_key_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
-    size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
-    desc_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
-
-    do_keywords = TRUE;
-
-#define VIEWS(_views) {_views}
-#define VIEW(_ident, _title, _pages) {_pages}
-#define PAGE(_columns) {_columns}
-#define COLUMN(_groups) {_groups}
-#define GROUP(_title, _items) {_items}
-
-#include "gb-shortcuts-window.defs"
-
-#undef VIEWS
-#undef VIEW
-#undef PAGE
-#undef COLUMN
-#undef GROUP
-#undef SHORTCUT
-#undef GESTURE
-
-    g_clear_object (&mod_key_group);
-    g_clear_object (&size_group);
-    g_clear_object (&desc_group);
-  }
-}
-
-static void
-gb_shortcuts_window__search_entry__changed (GbShortcutsWindow *self,
-                                            GtkSearchEntry    *search_entry)
-{
-  g_autoptr(IdePatternSpec) spec = NULL;
-  GHashTableIter iter;
-  g_autofree gchar *query = NULL;
-  g_autofree gchar *name = NULL;
-  const gchar *text;
-  GtkWidget *visible_child;
-  gpointer key;
-  gpointer value;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
-
-  text = gtk_entry_get_text (GTK_ENTRY (search_entry));
-
-  if (ide_str_empty0 (text))
-    {
-      if (self->previous_view != NULL)
-        gtk_stack_set_visible_child (self->stack, self->previous_view);
-      g_hash_table_foreach (self->widget_keywords,
-                            (GHFunc)gtk_widget_show,
-                            NULL);
-      return;
-    }
-
-  visible_child = gtk_stack_get_visible_child (self->stack);
-  gtk_container_child_get (GTK_CONTAINER (self->stack), visible_child,
-                           "name", &name,
-                           NULL);
-
-  if (!ide_str_equal0 (name, "internal-search"))
-    {
-      self->previous_view = visible_child;
-      gtk_stack_set_visible_child_name (self->stack, "internal-search");
-    }
-
-  query = g_utf8_casefold (text, -1);
-  spec = ide_pattern_spec_new (query);
-
-  g_hash_table_iter_init (&iter, self->widget_keywords);
-
-  while (g_hash_table_iter_next (&iter, &key, &value))
-    {
-      const gchar *keywords = value;
-
-      if (ide_pattern_spec_match (spec, keywords))
-        gtk_widget_show (key);
-      else
-        gtk_widget_hide (key);
-    }
-}
-
-static void
-gb_shortcuts_window__stack__notify_visible_child (GbShortcutsWindow *self,
-                                                  GParamSpec        *pspec,
-                                                  GtkStack          *stack)
-{
-  g_autofree gchar *title = NULL;
-  GtkWidget *visible_child;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-  g_assert (GTK_IS_STACK (stack));
-
-  visible_child = gtk_stack_get_visible_child (stack);
-  gtk_container_child_get (GTK_CONTAINER (stack), visible_child,
-                           "title", &title,
-                           NULL);
-  gtk_label_set_label (self->menu_label, title);
-}
-
-static void
-gb_shortcuts_window__list_box__row_activated (GbShortcutsWindow *self,
-                                              GtkListBoxRow     *row,
-                                              GtkListBox        *list_box)
-{
-  const gchar *view;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-  g_assert (GTK_IS_LIST_BOX_ROW (row));
-  g_assert (GTK_IS_LIST_BOX (list_box));
-
-  view = g_object_get_data (G_OBJECT (row), "view");
-  if (view != NULL)
-    gtk_stack_set_visible_child_name (self->stack, view);
-
-  /*
-   * Ensure search is now hidden.
-   */
-  gtk_entry_set_text (GTK_ENTRY (self->search_entry), "");
-  egg_search_bar_set_search_mode_enabled (self->search_bar, FALSE);
-
-  /*
-   * Apparently we need to hide the popover manually.
-   */
-  gtk_widget_hide (GTK_WIDGET (self->popover));
-}
-
-static void
-gb_shortcuts_window_constructed (GObject *object)
-{
-  GbShortcutsWindow *self = (GbShortcutsWindow *)object;
-
-  g_assert (GB_IS_SHORTCUTS_WINDOW (self));
-
-  G_OBJECT_CLASS (gb_shortcuts_window_parent_class)->constructed (object);
-
-  gb_shortcuts_window_set_view (self, "editor");
-}
-
-static void
-gb_shortcuts_window_finalize (GObject *object)
-{
-  GbShortcutsWindow *self = (GbShortcutsWindow *)object;
-
-  g_clear_pointer (&self->widget_keywords, g_hash_table_unref);
-
-  G_OBJECT_CLASS (gb_shortcuts_window_parent_class)->finalize (object);
-}
+G_DEFINE_TYPE (GbShortcutsWindow, gb_shortcuts_window, GB_TYPE_SHORTCUTS_DIALOG)
 
 static void
 gb_shortcuts_window_class_init (GbShortcutsWindowClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-  GtkBindingSet *binding_set;
-
-  object_class->constructed = gb_shortcuts_window_constructed;
-  object_class->finalize = gb_shortcuts_window_finalize;
-
-  gSignals [CLOSE] =
-    g_signal_new_class_handler ("close",
-                                G_TYPE_FROM_CLASS (klass),
-                                (G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION),
-                                G_CALLBACK (gtk_window_close),
-                                NULL, NULL, NULL, G_TYPE_NONE, 0);
-
-  binding_set = gtk_binding_set_by_class (klass);
-  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0);
 
   gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/gb-shortcuts-window.ui");
-
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, list_box);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, menu_button);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, menu_label);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, popover);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, search_bar);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, search_entry);
-  gtk_widget_class_bind_template_child (widget_class, GbShortcutsWindow, stack);
-
-  g_type_ensure (EGG_TYPE_SEARCH_BAR);
-  g_type_ensure (GB_TYPE_SCROLLED_WINDOW);
 }
 
 static void
 gb_shortcuts_window_init (GbShortcutsWindow *self)
 {
-  self->widget_keywords = g_hash_table_new_full (NULL, NULL, NULL, g_free);
-
   gtk_widget_init_template (GTK_WIDGET (self));
-
-  gb_shortcuts_window_build (self);
-
-  g_signal_connect_object (self->search_entry,
-                           "changed",
-                           G_CALLBACK (gb_shortcuts_window__search_entry__changed),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  g_signal_connect_object (self->stack,
-                           "notify::visible-child",
-                           G_CALLBACK (gb_shortcuts_window__stack__notify_visible_child),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  g_signal_connect_object (self->list_box,
-                           "row-activated",
-                           G_CALLBACK (gb_shortcuts_window__list_box__row_activated),
-                           self,
-                           G_CONNECT_SWAPPED);
 }
diff --git a/src/keybindings/gb-shortcuts-window.h b/src/keybindings/gb-shortcuts-window.h
index ec9e6a0..93649da 100644
--- a/src/keybindings/gb-shortcuts-window.h
+++ b/src/keybindings/gb-shortcuts-window.h
@@ -19,15 +19,13 @@
 #ifndef GB_SHORTCUTS_WINDOW_H
 #define GB_SHORTCUTS_WINDOW_H
 
-#include <gtk/gtk.h>
+#include "gb-shortcuts-dialog.h"
 
 G_BEGIN_DECLS
 
 #define GB_TYPE_SHORTCUTS_WINDOW (gb_shortcuts_window_get_type())
 
-G_DECLARE_FINAL_TYPE (GbShortcutsWindow, gb_shortcuts_window, GB, SHORTCUTS_WINDOW, GtkWindow)
-
-GtkWidget *gb_shortcuts_window_new (void);
+G_DECLARE_FINAL_TYPE (GbShortcutsWindow, gb_shortcuts_window, GB, SHORTCUTS_WINDOW, GbShortcutsDialog)
 
 G_END_DECLS
 
diff --git a/src/shortcuts/gb-shortcuts-column.c b/src/shortcuts/gb-shortcuts-column.c
new file mode 100644
index 0000000..1407e67
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-column.c
@@ -0,0 +1,38 @@
+/* gb-shortcuts-column.c
+ *
+ * Copyright (C) 2015 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 "gb-shortcuts-column.h"
+
+struct _GbShortcutsColumn
+{
+  GtkBox parent_instance;
+};
+
+G_DEFINE_TYPE (GbShortcutsColumn, gb_shortcuts_column, GTK_TYPE_BOX)
+
+static void
+gb_shortcuts_column_class_init (GbShortcutsColumnClass *klass)
+{
+}
+
+static void
+gb_shortcuts_column_init (GbShortcutsColumn *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
+  gtk_box_set_spacing (GTK_BOX (self), 22);
+}
diff --git a/src/shortcuts/gb-shortcuts-column.h b/src/shortcuts/gb-shortcuts-column.h
new file mode 100644
index 0000000..6f80406
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-column.h
@@ -0,0 +1,32 @@
+/* gb-shortcuts-column.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_COLUMN_H
+#define GB_SHORTCUTS_COLUMN_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_COLUMN (gb_shortcuts_column_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsColumn, gb_shortcuts_column, GB, SHORTCUTS_COLUMN, GtkBox)
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_COLUMN_H */
diff --git a/src/shortcuts/gb-shortcuts-dialog.c b/src/shortcuts/gb-shortcuts-dialog.c
new file mode 100644
index 0000000..653ec07
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-dialog.c
@@ -0,0 +1,273 @@
+/* gb-shortcuts-dialog.c
+ *
+ * Copyright (C) 2015 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 <glib/gi18n.h>
+
+#include "egg-search-bar.h"
+
+#include "gb-scrolled-window.h"
+#include "gb-shortcuts-column.h"
+#include "gb-shortcuts-dialog.h"
+#include "gb-shortcuts-gesture.h"
+#include "gb-shortcuts-group.h"
+#include "gb-shortcuts-page.h"
+#include "gb-shortcuts-shortcut.h"
+#include "gb-shortcuts-view.h"
+
+typedef struct
+{
+  GtkStack       *stack;
+  GtkMenuButton  *menu_button;
+  GtkLabel       *menu_label;
+  EggSearchBar   *search_bar;
+  GtkSearchEntry *search_entry;
+  GtkHeaderBar   *header_bar;
+  GtkPopover     *popover;
+  GtkListBox     *list_box;
+} GbShortcutsDialogPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GbShortcutsDialog, gb_shortcuts_dialog, GTK_TYPE_WINDOW)
+
+enum {
+  CLOSE,
+  LAST_SIGNAL
+};
+
+static guint gSignals [LAST_SIGNAL];
+
+static void
+gb_shortcuts_dialog_add_view (GbShortcutsDialog *self,
+                              GbShortcutsView   *view)
+{
+  GbShortcutsDialogPrivate *priv = gb_shortcuts_dialog_get_instance_private (self);
+  GtkListBoxRow *row;
+  const gchar *title;
+  const gchar *name;
+  GtkWidget *label;
+
+  g_assert (GB_IS_SHORTCUTS_DIALOG (self));
+  g_assert (GB_IS_SHORTCUTS_VIEW (view));
+
+  name = gb_shortcuts_view_get_view_name (view);
+  title = gb_shortcuts_view_get_title (view);
+
+  gtk_stack_add_titled (priv->stack, GTK_WIDGET (view), name, title);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data_full (G_OBJECT (row), "GB_SHORTCUTS_VIEW_NAME", g_strdup (name), g_free);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "margin", 6,
+                        "label", title,
+                        "xalign", 0.0f,
+                        "visible", TRUE,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label));
+  gtk_container_add (GTK_CONTAINER (priv->list_box), GTK_WIDGET (row));
+}
+
+static void
+gb_shortcuts_dialog_add (GtkContainer *container,
+                         GtkWidget    *widget)
+{
+  GbShortcutsDialog *self = (GbShortcutsDialog *)container;
+
+  g_assert (GB_IS_SHORTCUTS_DIALOG (self));
+
+  if (GB_IS_SHORTCUTS_VIEW (widget))
+    gb_shortcuts_dialog_add_view (self, GB_SHORTCUTS_VIEW (widget));
+  else
+    GTK_CONTAINER_CLASS (gb_shortcuts_dialog_parent_class)->add (container, widget);
+}
+
+static void
+gb_shortcuts_dialog__stack__notify_visible_child (GbShortcutsDialog *self,
+                                                  GParamSpec        *pspec,
+                                                  GtkStack          *stack)
+{
+  GbShortcutsDialogPrivate *priv = gb_shortcuts_dialog_get_instance_private (self);
+  GtkWidget *visible_child;
+
+  visible_child = gtk_stack_get_visible_child (stack);
+
+  if (GB_IS_SHORTCUTS_VIEW (visible_child))
+    {
+      const gchar *title;
+
+      title = gb_shortcuts_view_get_title (GB_SHORTCUTS_VIEW (visible_child));
+      gtk_label_set_label (priv->menu_label, title);
+    }
+}
+
+static void
+gb_shortcuts_dialog__list_box__row_activated (GbShortcutsDialog *self,
+                                              GtkListBoxRow     *row,
+                                              GtkListBox        *list_box)
+{
+  GbShortcutsDialogPrivate *priv = gb_shortcuts_dialog_get_instance_private (self);
+  const gchar *name;
+
+  g_assert (GB_IS_SHORTCUTS_DIALOG (self));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  name = g_object_get_data (G_OBJECT (row), "GB_SHORTCUTS_VIEW_NAME");
+  gtk_stack_set_visible_child_name (priv->stack, name);
+  gtk_widget_hide (GTK_WIDGET (priv->popover));
+}
+
+static void
+gb_shortcuts_dialog_class_init (GbShortcutsDialogClass *klass)
+{
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  GtkBindingSet *binding_set = gtk_binding_set_by_class (klass);
+
+  container_class->add = gb_shortcuts_dialog_add;
+
+  gSignals [CLOSE] = g_signal_new ("close",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                   G_STRUCT_OFFSET (GbShortcutsDialogClass, close),
+                                   NULL, NULL, NULL,
+                                   G_TYPE_NONE,
+                                   0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0);
+
+  g_type_ensure (GB_TYPE_SHORTCUTS_PAGE);
+  g_type_ensure (GB_TYPE_SHORTCUTS_COLUMN);
+  g_type_ensure (GB_TYPE_SHORTCUTS_GROUP);
+  g_type_ensure (GB_TYPE_SHORTCUTS_GESTURE);
+  g_type_ensure (GB_TYPE_SHORTCUTS_SHORTCUT);
+}
+
+static void
+gb_shortcuts_dialog_init (GbShortcutsDialog *self)
+{
+  GbShortcutsDialogPrivate *priv = gb_shortcuts_dialog_get_instance_private (self);
+  GtkToggleButton *search_button;
+  GtkScrolledWindow *scroller;
+  GtkBox *main_box;
+  GtkBox *menu_box;
+  GtkArrow *arrow;
+  GtkSearchEntry *entry;
+
+  priv->header_bar = g_object_new (GTK_TYPE_HEADER_BAR,
+                                   "show-close-button", TRUE,
+                                   "visible", TRUE,
+                                   NULL);
+  gtk_window_set_titlebar (GTK_WINDOW (self), GTK_WIDGET (priv->header_bar));
+
+  search_button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                                "child", g_object_new (GTK_TYPE_IMAGE,
+                                                       "visible", TRUE,
+                                                       "icon-name", "edit-find-symbolic",
+                                                       NULL),
+                                "visible", TRUE,
+                                NULL);
+  gtk_container_add (GTK_CONTAINER (priv->header_bar), GTK_WIDGET (search_button));
+
+  main_box = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_VERTICAL,
+                           "visible", TRUE,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (main_box));
+
+  priv->search_bar = g_object_new (EGG_TYPE_SEARCH_BAR,
+                                   "visible", TRUE,
+                                   NULL);
+  g_object_bind_property (priv->search_bar, "search-mode-enabled",
+                          search_button, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+  gtk_container_add (GTK_CONTAINER (main_box), GTK_WIDGET (priv->search_bar));
+
+  priv->stack = g_object_new (GTK_TYPE_STACK,
+                              "expand", TRUE,
+                              "homogeneous", TRUE,
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (main_box), GTK_WIDGET (priv->stack));
+
+  priv->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON,
+                                    "focus-on-click", FALSE,
+                                    "visible", TRUE,
+                                    NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (priv->menu_button)),
+                               "flat");
+  gtk_header_bar_set_custom_title (priv->header_bar, GTK_WIDGET (priv->menu_button));
+
+  menu_box = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_HORIZONTAL,
+                           "spacing", 3,
+                           "visible", TRUE,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (priv->menu_button), GTK_WIDGET (menu_box));
+
+  priv->menu_label = g_object_new (GTK_TYPE_LABEL,
+                                   "visible", TRUE,
+                                   NULL);
+  gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (priv->menu_label));
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  arrow = g_object_new (GTK_TYPE_ARROW,
+                        "arrow-type", GTK_ARROW_DOWN,
+                        "visible", TRUE,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (menu_box), GTK_WIDGET (arrow));
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  priv->popover = g_object_new (GTK_TYPE_POPOVER,
+                                "border-width", 6,
+                                "relative-to", priv->menu_button,
+                                "position", GTK_POS_BOTTOM,
+                                NULL);
+  gtk_menu_button_set_popover (priv->menu_button, GTK_WIDGET (priv->popover));
+
+  scroller = g_object_new (GB_TYPE_SCROLLED_WINDOW,
+                           "min-content-width", 150,
+                           "max-content-width", 300,
+                           "min-content-height", 10,
+                           "max-content-height", 300,
+                           "shadow-type", GTK_SHADOW_IN,
+                           "visible", TRUE,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (priv->popover), GTK_WIDGET (scroller));
+
+  priv->list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                                 "selection-mode", GTK_SELECTION_NONE,
+                                 "visible", TRUE,
+                                 NULL);
+  g_signal_connect_object (priv->list_box,
+                           "row-activated",
+                           G_CALLBACK (gb_shortcuts_dialog__list_box__row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (priv->list_box));
+
+  entry = GTK_SEARCH_ENTRY (egg_search_bar_get_entry (priv->search_bar));
+  g_object_set (entry,
+                "placeholder-text", _("Search Shortcuts"),
+                "width-chars", 40,
+                NULL);
+
+  g_signal_connect_object (priv->stack,
+                           "notify::visible-child",
+                           G_CALLBACK (gb_shortcuts_dialog__stack__notify_visible_child),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
diff --git a/src/shortcuts/gb-shortcuts-dialog.h b/src/shortcuts/gb-shortcuts-dialog.h
new file mode 100644
index 0000000..28e98a4
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-dialog.h
@@ -0,0 +1,39 @@
+/* gb-shortcuts-dialog.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_DIALOG_H
+#define GB_SHORTCUTS_DIALOG_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_DIALOG (gb_shortcuts_dialog_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (GbShortcutsDialog, gb_shortcuts_dialog, GB, SHORTCUTS_DIALOG, GtkWindow)
+
+struct _GbShortcutsDialogClass
+{
+  GtkWindowClass parent_class;
+
+  void (*close) (GbShortcutsDialog *self);
+};
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_DIALOG_H */
diff --git a/src/shortcuts/gb-shortcuts-gesture.c b/src/shortcuts/gb-shortcuts-gesture.c
new file mode 100644
index 0000000..d00c8c5
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-gesture.c
@@ -0,0 +1,194 @@
+/* gb-shortcuts-gesture.c
+ *
+ * Copyright (C) 2015 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 "gb-shortcuts-gesture.h"
+
+struct _GbShortcutsGesture
+{
+  GtkBox    parent_instance;
+
+  GtkImage *image;
+  GtkLabel *title;
+  GtkLabel *subtitle;
+  GtkBox   *desc_box;
+};
+
+G_DEFINE_TYPE (GbShortcutsGesture, gb_shortcuts_gesture, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_DESC_SIZE_GROUP,
+  PROP_ICON_NAME,
+  PROP_ICON_SIZE_GROUP,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void
+gb_shortcuts_gesture_set_icon_name (GbShortcutsGesture *self,
+                                    const gchar        *icon_name)
+{
+  g_autofree gchar *basedir = NULL;
+  g_autofree gchar *path = NULL;
+  g_autoptr(GBytes) bytes = NULL;
+
+  g_assert (GB_IS_SHORTCUTS_GESTURE (self));
+
+  if (icon_name == NULL)
+    {
+      g_object_set (self->image, "icon-name", NULL, NULL);
+      return;
+    }
+
+  basedir = g_build_filename (g_application_get_resource_base_path (g_application_get_default ()),
+                              "icons", "scalable", "actions",
+                              NULL);
+  path = g_strdup_printf ("%s"G_DIR_SEPARATOR_S"%s.svg", basedir, icon_name);
+  bytes = g_resources_lookup_data (path, 0, NULL);
+
+  if (bytes != NULL)
+    g_object_set (self->image, "resource", path, NULL);
+  else
+    g_object_set (self->image, "icon-name", icon_name, NULL);
+}
+
+static void
+gb_shortcuts_gesture_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GbShortcutsGesture *self = GB_SHORTCUTS_GESTURE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DESC_SIZE_GROUP:
+      {
+        GtkSizeGroup *group = g_value_get_object (value);
+
+        if (group != NULL)
+          gtk_size_group_add_widget (group, GTK_WIDGET (self->desc_box));
+        break;
+      }
+
+    case PROP_ICON_NAME:
+      gb_shortcuts_gesture_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON_SIZE_GROUP:
+      {
+        GtkSizeGroup *group = g_value_get_object (value);
+
+        if (group != NULL)
+          gtk_size_group_add_widget (group, GTK_WIDGET (self->image));
+        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
+gb_shortcuts_gesture_class_init (GbShortcutsGestureClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = gb_shortcuts_gesture_set_property;
+
+  gParamSpecs [PROP_DESC_SIZE_GROUP] =
+    g_param_spec_object ("desc-size-group",
+                         "Description Size Group",
+                         "Description Size Group",
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "Icon Name",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_ICON_SIZE_GROUP] =
+    g_param_spec_object ("icon-size-group",
+                         "Icon Size Group",
+                         "Icon Size Group",
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "Subtitle",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+gb_shortcuts_gesture_init (GbShortcutsGesture *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL);
+  gtk_box_set_spacing (GTK_BOX (self), 12);
+
+  self->image = g_object_new (GTK_TYPE_IMAGE,
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->image));
+
+  self->desc_box = g_object_new (GTK_TYPE_BOX,
+                                 "hexpand", TRUE,
+                                 "orientation", GTK_ORIENTATION_VERTICAL,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->desc_box));
+
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self->desc_box), GTK_WIDGET (self->title));
+
+  self->subtitle = g_object_new (GTK_TYPE_LABEL,
+                                 "visible", TRUE,
+                                 "xalign", 0.0f,
+                                 NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->subtitle)),
+                               "dim-label");
+  gtk_container_add (GTK_CONTAINER (self->desc_box), GTK_WIDGET (self->subtitle));
+}
diff --git a/src/shortcuts/gb-shortcuts-gesture.h b/src/shortcuts/gb-shortcuts-gesture.h
new file mode 100644
index 0000000..aa4d3d2
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-gesture.h
@@ -0,0 +1,32 @@
+/* gb-shortcuts-gesture.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_GESTURE_H
+#define GB_SHORTCUTS_GESTURE_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_GESTURE (gb_shortcuts_gesture_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsGesture, gb_shortcuts_gesture, GB, SHORTCUTS_GESTURE, GtkBox)
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_GESTURE_H */
diff --git a/src/shortcuts/gb-shortcuts-group.c b/src/shortcuts/gb-shortcuts-group.c
new file mode 100644
index 0000000..e4ce9ff
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-group.c
@@ -0,0 +1,111 @@
+/* gb-shortcuts-group.c
+ *
+ * Copyright (C) 2015 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 "gb-shortcuts-group.h"
+
+struct _GbShortcutsGroup
+{
+  GtkBox    parent_instance;
+
+  GtkLabel *title;
+};
+
+G_DEFINE_TYPE (GbShortcutsGroup, gb_shortcuts_group, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void
+gb_shortcuts_group_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  GbShortcutsGroup *self = GB_SHORTCUTS_GROUP (object);
+
+  switch (prop_id)
+    {
+    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
+gb_shortcuts_group_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  GbShortcutsGroup *self = GB_SHORTCUTS_GROUP (object);
+
+  switch (prop_id)
+    {
+    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
+gb_shortcuts_group_class_init (GbShortcutsGroupClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = gb_shortcuts_group_get_property;
+  object_class->set_property = gb_shortcuts_group_set_property;
+
+  gParamSpecs [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+gb_shortcuts_group_init (GbShortcutsGroup *self)
+{
+  PangoAttrList *attrs;
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
+  gtk_box_set_spacing (GTK_BOX (self), 10);
+
+  attrs = pango_attr_list_new ();
+  pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "attributes", attrs,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->title));
+  pango_attr_list_unref (attrs);
+}
diff --git a/src/shortcuts/gb-shortcuts-group.h b/src/shortcuts/gb-shortcuts-group.h
new file mode 100644
index 0000000..bd164c6
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-group.h
@@ -0,0 +1,32 @@
+/* gb-shortcuts-group.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_GROUP_H
+#define GB_SHORTCUTS_GROUP_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_GROUP (gb_shortcuts_group_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsGroup, gb_shortcuts_group, GB, SHORTCUTS_GROUP, GtkBox)
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_GROUP_H */
diff --git a/src/shortcuts/gb-shortcuts-page.c b/src/shortcuts/gb-shortcuts-page.c
new file mode 100644
index 0000000..4e0af7f
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-page.c
@@ -0,0 +1,37 @@
+/* gb-shortcuts-page.c
+ *
+ * Copyright (C) 2015 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 "gb-shortcuts-page.h"
+
+struct _GbShortcutsPage
+{
+  GtkBox parent_instance;
+};
+
+G_DEFINE_TYPE (GbShortcutsPage, gb_shortcuts_page, GTK_TYPE_BOX)
+
+static void
+gb_shortcuts_page_class_init (GbShortcutsPageClass *klass)
+{
+}
+
+static void
+gb_shortcuts_page_init (GbShortcutsPage *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL);
+}
diff --git a/src/shortcuts/gb-shortcuts-page.h b/src/shortcuts/gb-shortcuts-page.h
new file mode 100644
index 0000000..084122a
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-page.h
@@ -0,0 +1,32 @@
+/* gb-shortcuts-page.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_PAGE_H
+#define GB_SHORTCUTS_PAGE_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_PAGE (gb_shortcuts_page_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsPage, gb_shortcuts_page, GB, SHORTCUTS_PAGE, GtkBin)
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_PAGE_H */
diff --git a/src/shortcuts/gb-shortcuts-shortcut.c b/src/shortcuts/gb-shortcuts-shortcut.c
new file mode 100644
index 0000000..fc8889e
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-shortcut.c
@@ -0,0 +1,139 @@
+/* gb-shortcuts-shortcut.c
+ *
+ * Copyright (C) 2015 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 "gb-accel-label.h"
+#include "gb-shortcuts-shortcut.h"
+
+struct _GbShortcutsShortcut
+{
+  GtkBox parent_instance;
+
+  GbAccelLabel *accelerator;
+  GtkLabel     *title;
+};
+
+G_DEFINE_TYPE (GbShortcutsShortcut, gb_shortcuts_shortcut, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_ACCELERATOR,
+  PROP_ACCELERATOR_SIZE_GROUP,
+  PROP_TITLE,
+  PROP_TITLE_SIZE_GROUP,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void
+gb_shortcuts_shortcut_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  GbShortcutsShortcut *self = GB_SHORTCUTS_SHORTCUT (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      gb_accel_label_set_accelerator (self->accelerator, g_value_get_string (value));
+      break;
+
+    case PROP_ACCELERATOR_SIZE_GROUP:
+      {
+        GtkSizeGroup *group = g_value_get_object (value);
+
+        if (group != NULL)
+          gtk_size_group_add_widget (group, GTK_WIDGET (self->accelerator));
+        break;
+      }
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE_SIZE_GROUP:
+      {
+        GtkSizeGroup *group = g_value_get_object (value);
+
+        if (group != NULL)
+          gtk_size_group_add_widget (group, GTK_WIDGET (self->title));
+        break;
+      }
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_shortcuts_shortcut_class_init (GbShortcutsShortcutClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = gb_shortcuts_shortcut_set_property;
+
+  gParamSpecs [PROP_ACCELERATOR] =
+    g_param_spec_string ("accelerator",
+                         "Accelerator",
+                         "Accelerator",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_ACCELERATOR_SIZE_GROUP] =
+    g_param_spec_object ("accelerator-size-group",
+                         "Accelerator Size Group",
+                         "Accelerator Size Group",
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_TITLE_SIZE_GROUP] =
+    g_param_spec_object ("title-size-group",
+                         "Title Size Group",
+                         "Title Size Group",
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+gb_shortcuts_shortcut_init (GbShortcutsShortcut *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL);
+  gtk_box_set_spacing (GTK_BOX (self), 12);
+
+  self->accelerator = g_object_new (GB_TYPE_ACCEL_LABEL,
+                                    "visible", TRUE,
+                                    NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->accelerator));
+
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "hexpand", TRUE,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->title));
+}
diff --git a/src/shortcuts/gb-shortcuts-shortcut.h b/src/shortcuts/gb-shortcuts-shortcut.h
new file mode 100644
index 0000000..b4da11a
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-shortcut.h
@@ -0,0 +1,32 @@
+/* gb-shortcuts-shortcut.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_SHORTCUT_H
+#define GB_SHORTCUTS_SHORTCUT_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_SHORTCUT (gb_shortcuts_shortcut_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsShortcut, gb_shortcuts_shortcut, GB, SHORTCUTS_SHORTCUT, GtkBox)
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_SHORTCUT_H */
diff --git a/src/shortcuts/gb-shortcuts-view.c b/src/shortcuts/gb-shortcuts-view.c
new file mode 100644
index 0000000..74c2901
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-view.c
@@ -0,0 +1,215 @@
+/* gb-shortcuts-view.c
+ *
+ * Copyright (C) 2015 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 "gb-shortcuts-page.h"
+#include "gb-shortcuts-view.h"
+
+struct _GbShortcutsView
+{
+  GtkBox            parent_instance;
+
+  gchar            *name;
+  gchar            *title;
+
+  GtkStack         *stack;
+  GtkStackSwitcher *switcher;
+
+  guint             last_page_num;
+};
+
+G_DEFINE_TYPE (GbShortcutsView, gb_shortcuts_view, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_VIEW_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void
+adjust_page_buttons (GtkWidget *widget,
+                     gpointer   data)
+{
+  gint *count = data;
+
+  /*
+   * TODO: This is a hack to get the GtkStackSwitcher radio
+   *       buttons to look how we want. However, it's very
+   *       much font size specific.
+   */
+  gtk_widget_set_size_request (widget, 34, 34);
+
+  (*count)++;
+}
+
+static void
+gb_shortcuts_view_add (GtkContainer *container,
+                       GtkWidget    *child)
+{
+  GbShortcutsView *self = (GbShortcutsView *)container;
+
+  g_assert (GB_IS_SHORTCUTS_VIEW (self));
+
+  if (GB_IS_SHORTCUTS_PAGE (child))
+    {
+      g_autofree gchar *title = NULL;
+      guint count = 0;
+
+      title = g_strdup_printf ("%u", ++self->last_page_num);
+      gtk_container_add_with_properties (GTK_CONTAINER (self->stack), child,
+                                         "title", title,
+                                         NULL);
+
+      gtk_container_foreach (GTK_CONTAINER (self->switcher), adjust_page_buttons, &count);
+      gtk_widget_set_visible (GTK_WIDGET (self->switcher), (count > 1));
+    }
+  else
+    {
+      GTK_CONTAINER_CLASS (gb_shortcuts_view_parent_class)->add (container, child);
+    }
+}
+
+static void
+gb_shortcuts_view_finalize (GObject *object)
+{
+  GbShortcutsView *self = (GbShortcutsView *)object;
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (gb_shortcuts_view_parent_class)->finalize (object);
+}
+
+static void
+gb_shortcuts_view_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  GbShortcutsView *self = (GbShortcutsView *)object;
+
+  switch (prop_id)
+    {
+    case PROP_VIEW_NAME:
+      g_value_set_string (value, self->name);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_shortcuts_view_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  GbShortcutsView *self = (GbShortcutsView *)object;
+
+  switch (prop_id)
+    {
+    case PROP_VIEW_NAME:
+      g_free (self->name);
+      self->name = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      g_free (self->title);
+      self->title = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_shortcuts_view_class_init (GbShortcutsViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = gb_shortcuts_view_finalize;
+  object_class->get_property = gb_shortcuts_view_get_property;
+  object_class->set_property = gb_shortcuts_view_set_property;
+
+  container_class->add = gb_shortcuts_view_add;
+
+  gParamSpecs [PROP_VIEW_NAME] =
+    g_param_spec_string ("view-name",
+                         "View Name",
+                         "View Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+gb_shortcuts_view_init (GbShortcutsView *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
+  gtk_box_set_homogeneous (GTK_BOX (self), FALSE);
+  gtk_box_set_spacing (GTK_BOX (self), 22);
+  gtk_container_set_border_width (GTK_CONTAINER (self), 24);
+
+  self->stack = g_object_new (GTK_TYPE_STACK,
+                              "homogeneous", TRUE,
+                              "vexpand", TRUE,
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->stack));
+
+  self->switcher = g_object_new (GTK_TYPE_STACK_SWITCHER,
+                                 "halign", GTK_ALIGN_CENTER,
+                                 "stack", self->stack,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->switcher)), "round");
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->switcher));
+}
+
+const gchar *
+gb_shortcuts_view_get_view_name  (GbShortcutsView *self)
+{
+  g_return_val_if_fail (GB_IS_SHORTCUTS_VIEW (self), NULL);
+
+  return self->name;
+}
+
+const gchar *
+gb_shortcuts_view_get_title (GbShortcutsView *self)
+{
+  g_return_val_if_fail (GB_IS_SHORTCUTS_VIEW (self), NULL);
+
+  return self->title;
+}
diff --git a/src/shortcuts/gb-shortcuts-view.h b/src/shortcuts/gb-shortcuts-view.h
new file mode 100644
index 0000000..c0ad195
--- /dev/null
+++ b/src/shortcuts/gb-shortcuts-view.h
@@ -0,0 +1,35 @@
+/* gb-shortcuts-view.h
+ *
+ * Copyright (C) 2015 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 GB_SHORTCUTS_VIEW_H
+#define GB_SHORTCUTS_VIEW_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_SHORTCUTS_VIEW (gb_shortcuts_view_get_type())
+
+G_DECLARE_FINAL_TYPE (GbShortcutsView, gb_shortcuts_view, GB, SHORTCUTS_VIEW, GtkBox)
+
+const gchar *gb_shortcuts_view_get_view_name (GbShortcutsView *self);
+const gchar *gb_shortcuts_view_get_title     (GbShortcutsView *self);
+
+G_END_DECLS
+
+#endif /* GB_SHORTCUTS_VIEW_H */


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