[gnome-builder/wip/chergert/merge-shortcuts] shortcuts: import shortcut engine



commit d7e11fb1cc3477d1b8f7d7beeb4402ad56b5d3fc
Author: Christian Hergert <chergert redhat com>
Date:   Mon May 22 19:13:07 2017 -0700

    shortcuts: import shortcut engine
    
    This adds my gtk-shortcut-engine into Builder using the Ide namespace.
    There is a copy_and_paste.py script in the shortcut-engine prototype
    repository to import the sources into a project (in this case Builder)
    with a given prefix. I find this cleaner, for now, than creating yet
    another library that plugins and things would need to link against.
    
    As an asside, I'd like to make a lot of things just be in Builder and
    get down to a single binary, but that will take a lot of work.
    
    Anyway, this does not get things wired up into Builder, it only imports
    the project and gets things compiling and introspection working.

 libide/Makefile.am                                |   33 +-
 libide/resources/libide.gresource.xml             |    6 +
 libide/shortcuts/enter-keyboard-shortcut.svg      |  245 +++++
 libide/shortcuts/ide-shortcut-accel-dialog.c      |  528 ++++++++++
 libide/shortcuts/ide-shortcut-accel-dialog.h      |   43 +
 libide/shortcuts/ide-shortcut-accel-dialog.ui     |   90 ++
 libide/shortcuts/ide-shortcut-chord.c             |  630 ++++++++++++
 libide/shortcuts/ide-shortcut-chord.h             |   81 ++
 libide/shortcuts/ide-shortcut-context.c           |  751 +++++++++++++++
 libide/shortcuts/ide-shortcut-context.h           |   66 ++
 libide/shortcuts/ide-shortcut-controller.c        |  781 +++++++++++++++
 libide/shortcuts/ide-shortcut-controller.h        |   52 +
 libide/shortcuts/ide-shortcut-label.c             |  212 ++++
 libide/shortcuts/ide-shortcut-label.h             |   42 +
 libide/shortcuts/ide-shortcut-manager.c           |  958 +++++++++++++++++++
 libide/shortcuts/ide-shortcut-manager.h           |   85 ++
 libide/shortcuts/ide-shortcut-model.c             |  331 +++++++
 libide/shortcuts/ide-shortcut-model.h             |   48 +
 libide/shortcuts/ide-shortcut-private.h           |   97 ++
 libide/shortcuts/ide-shortcut-theme-editor.c      |  477 +++++++++
 libide/shortcuts/ide-shortcut-theme-editor.h      |   53 +
 libide/shortcuts/ide-shortcut-theme-editor.ui     |   82 ++
 libide/shortcuts/ide-shortcut-theme-load.c        |  665 +++++++++++++
 libide/shortcuts/ide-shortcut-theme-save.c        |  191 ++++
 libide/shortcuts/ide-shortcut-theme.c             |  387 ++++++++
 libide/shortcuts/ide-shortcut-theme.h             |  100 ++
 libide/shortcuts/ide-shortcuts-group.c            |  381 ++++++++
 libide/shortcuts/ide-shortcuts-group.h            |   41 +
 libide/shortcuts/ide-shortcuts-section.c          |  817 ++++++++++++++++
 libide/shortcuts/ide-shortcuts-section.h          |   40 +
 libide/shortcuts/ide-shortcuts-shortcut-private.h |   37 +
 libide/shortcuts/ide-shortcuts-shortcut.c         |  730 ++++++++++++++
 libide/shortcuts/ide-shortcuts-shortcut.h         |   78 ++
 libide/shortcuts/ide-shortcuts-window-private.h   |   38 +
 libide/shortcuts/ide-shortcuts-window.c           | 1063 +++++++++++++++++++++
 libide/shortcuts/ide-shortcuts-window.h           |   57 ++
 36 files changed, 10314 insertions(+), 2 deletions(-)
---
diff --git a/libide/Makefile.am b/libide/Makefile.am
index d20adf8..f971805 100644
--- a/libide/Makefile.am
+++ b/libide/Makefile.am
@@ -129,6 +129,19 @@ libide_1_0_la_public_headers =                                              \
        search/ide-search-provider.h                                        \
        search/ide-search-reducer.h                                         \
        search/ide-search-result.h                                          \
+       shortcuts/ide-shortcut-accel-dialog.h                               \
+       shortcuts/ide-shortcut-chord.h                                      \
+       shortcuts/ide-shortcut-context.h                                    \
+       shortcuts/ide-shortcut-controller.h                                 \
+       shortcuts/ide-shortcut-label.h                                      \
+       shortcuts/ide-shortcut-manager.h                                    \
+       shortcuts/ide-shortcut-model.h                                      \
+       shortcuts/ide-shortcuts-group.h                                     \
+       shortcuts/ide-shortcuts-section.h                                   \
+       shortcuts/ide-shortcuts-shortcut.h                                  \
+       shortcuts/ide-shortcuts-window.h                                    \
+       shortcuts/ide-shortcut-theme-editor.h                               \
+       shortcuts/ide-shortcut-theme.h                                      \
        snippets/ide-source-snippet-chunk.h                                 \
        snippets/ide-source-snippet-context.h                               \
        snippets/ide-source-snippet.h                                       \
@@ -317,6 +330,21 @@ libide_1_0_la_public_sources =                                              \
        search/ide-search-engine.c                                          \
        search/ide-search-provider.c                                        \
        search/ide-search-result.c                                          \
+       shortcuts/ide-shortcut-accel-dialog.c                               \
+       shortcuts/ide-shortcut-chord.c                                      \
+       shortcuts/ide-shortcut-context.c                                    \
+       shortcuts/ide-shortcut-controller.c                                 \
+       shortcuts/ide-shortcut-label.c                                      \
+       shortcuts/ide-shortcut-manager.c                                    \
+       shortcuts/ide-shortcut-model.c                                      \
+       shortcuts/ide-shortcuts-group.c                                     \
+       shortcuts/ide-shortcuts-section.c                                   \
+       shortcuts/ide-shortcuts-shortcut.c                                  \
+       shortcuts/ide-shortcuts-window.c                                    \
+       shortcuts/ide-shortcut-theme.c                                      \
+       shortcuts/ide-shortcut-theme-editor.c                               \
+       shortcuts/ide-shortcut-theme-load.c                                 \
+       shortcuts/ide-shortcut-theme-save.c                                 \
        snippets/ide-source-snippet-chunk.c                                 \
        snippets/ide-source-snippet-context.c                               \
        snippets/ide-source-snippet.c                                       \
@@ -469,8 +497,6 @@ libide_1_0_la_SOURCES =                                                     \
        ide-internal.h                                                      \
        keybindings/ide-keybindings.c                                       \
        keybindings/ide-keybindings.h                                       \
-       keybindings/ide-shortcuts-window.c                                  \
-       keybindings/ide-shortcuts-window.h                                  \
        modelines/ide-modelines-file-settings.c                             \
        modelines/ide-modelines-file-settings.h                             \
        modelines/modeline-parser.c                                         \
@@ -490,6 +516,9 @@ libide_1_0_la_SOURCES =                                                     \
        preferences/ide-preferences-perspective.h                           \
        runner/ide-run-manager-private.h                                    \
        search/ide-search-reducer.c                                         \
+       shortcuts/ide-shortcuts-window-private.h                            \
+       shortcuts/ide-shortcuts-shortcut-private.h                          \
+       shortcuts/ide-shortcut-private.h                                    \
        snippets/ide-source-snippet-completion-item.c                       \
        snippets/ide-source-snippet-completion-item.h                       \
        snippets/ide-source-snippet-completion-provider.c                   \
diff --git a/libide/resources/libide.gresource.xml b/libide/resources/libide.gresource.xml
index c6dd809..6909761 100644
--- a/libide/resources/libide.gresource.xml
+++ b/libide/resources/libide.gresource.xml
@@ -77,6 +77,8 @@
     <file compressed="true" 
alias="ide-preferences-perspective.ui">../preferences/ide-preferences-perspective.ui</file>
     <file compressed="true" 
alias="ide-preferences-spin-button.ui">../preferences/ide-preferences-spin-button.ui</file>
     <file compressed="true" alias="ide-preferences-switch.ui">../preferences/ide-preferences-switch.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-shortcut-accel-dialog.ui">../shortcuts/ide-shortcut-accel-dialog.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-shortcut-theme-editor.ui">../shortcuts/ide-shortcut-theme-editor.ui</file>
     <file compressed="true" alias="ide-run-button.ui">../runner/ide-run-button.ui</file>
     <file compressed="true" alias="ide-transfer-row.ui">../transfers/ide-transfer-row.ui</file>
     <file compressed="true" alias="ide-transfers-button.ui">../transfers/ide-transfers-button.ui</file>
@@ -111,4 +113,8 @@
     <file compressed="true" alias="ide-build-perspective.ui">../buildui/ide-build-perspective.ui</file>
     <file compressed="true" 
alias="ide-environment-editor-row.ui">../buildui/ide-environment-editor-row.ui</file>
   </gresource>
+
+  <gresource prefix="/org/gnome/builder/icons">
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="enter-keyboard-shortcut.svg">../shortcuts/enter-keyboard-shortcut.svg</file>
+  </gresource>
 </gresources>
diff --git a/libide/shortcuts/enter-keyboard-shortcut.svg b/libide/shortcuts/enter-keyboard-shortcut.svg
new file mode 100644
index 0000000..b7ce2e4
--- /dev/null
+++ b/libide/shortcuts/enter-keyboard-shortcut.svg
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   width="256"
+   height="72"
+   viewBox="0 0 256 72.000001"
+   id="svg3611"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="enter-keyboard-shortcut.svg">
+  <defs
+     id="defs3613" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="137.98997"
+     inkscape:cy="34.663602"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     units="px"
+     inkscape:window-width="1366"
+     inkscape:window-height="704"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata3616">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Camada 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-980.36216)">
+    <g
+       id="g3715"
+       transform="translate(-503.23415,689.94658)">
+      <path
+         d="m 509.66363,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 
1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 
1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50028 z"
+         id="path27275"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 543.62146,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 
1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 
1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50028 z"
+         id="path27277"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 577.57927,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 
1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 
1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50028 z"
+         id="path27279"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 611.12326,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 
1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 
1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50028 z"
+         id="path5218"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 645.08109,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 
1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 
1.99751,-13.05863 0.12565,-0.82161 0.69519,-1.70681 1.50031,-1.50028 z"
+         id="path5220"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 679.0389,325.47627 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19813 1.43308,0.67185 
1.50029,1.50028 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 1.33168,-8.70575 
1.99751,-13.05863 0.12565,-0.82161 0.69521,-1.70681 1.50031,-1.50028 z"
+         id="path5222"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 712.58289,325.47627 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19813 
1.43306,0.67185 1.50029,1.50028 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35287 
1.33167,-8.70575 1.99751,-13.05863 0.12565,-0.82161 0.69519,-1.70681 1.50029,-1.50028 z"
+         id="path4829"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 516.19044,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.2936,0 0.8072,-0.19812 
1.43306,0.67186 1.50028,1.50029 l 1.99752,13.05863 -1.50029,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 
1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z"
+         id="path3662"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 550.14827,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29358,0 0.8072,-0.19812 1.43307,0.67186 
1.50028,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 1.33168,-8.70575 
1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50029 z"
+         id="path3664"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 584.10608,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 
1.43308,0.67186 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 
1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50029 z"
+         id="path3666"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 617.65007,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.29359,0 0.80721,-0.19812 
1.43306,0.67186 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66584,-4.35288 
1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z"
+         id="path3668"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 651.6079,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 1.43308,0.67186 
1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70575 
1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.5003,-1.50029 z"
+         id="path3670"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 685.56571,335.26648 c 5.53,1.41851 18.51387,1.41851 24.29357,0 0.80721,-0.19812 
1.43308,0.67186 1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 
1.33168,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.6952,-1.70681 1.5003,-1.50029 z"
+         id="path3672"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 719.1097,335.26648 c 5.53002,1.41851 18.51389,1.41851 24.29359,0 0.80721,-0.19812 
1.43306,0.67186 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 
1.33167,-8.70575 1.99752,-13.05863 0.12564,-0.82161 0.69518,-1.70681 1.50028,-1.50029 z"
+         id="path3674"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 525.98066,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19812 
1.43306,0.67185 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 
1.33167,-8.70576 1.99752,-13.05863 0.12564,-0.82162 0.69518,-1.70681 1.50028,-1.50029 z"
+         id="path3676"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 559.93849,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 
1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70576 
1.99751,-13.05863 0.12565,-0.82162 0.69519,-1.70681 1.50031,-1.50029 z"
+         id="path3678"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 593.8963,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 
1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 1.33168,-8.70576 
1.99751,-13.05863 0.12565,-0.82162 0.69521,-1.70681 1.50031,-1.50029 z"
+         id="path3680"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 627.44029,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.80721,-0.19812 
1.43306,0.67185 1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28862,0 -1.50029,0 c 0.66583,-4.35288 
1.33167,-8.70576 1.99751,-13.05863 0.12565,-0.82162 0.69519,-1.70681 1.50029,-1.50029 z"
+         id="path3682"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 661.39812,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.80721,-0.19812 1.43308,0.67185 
1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33168,-8.70576 
1.99751,-13.05863 0.12564,-0.82162 0.69519,-1.70681 1.50031,-1.50029 z"
+         id="path3684"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 695.35593,345.0567 c 5.53,1.4185 18.51387,1.4185 24.29357,0 0.8072,-0.19812 1.43308,0.67185 
1.50029,1.50029 l 1.99753,13.05863 -1.5003,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33168,-8.70576 
1.99751,-13.05863 0.12564,-0.82162 0.69521,-1.70681 1.50031,-1.50029 z"
+         id="path3686"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.59999979;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <path
+         d="m 728.89992,345.0567 c 5.53002,1.4185 18.51389,1.4185 24.29359,0 0.8072,-0.19812 1.43306,0.67185 
1.50029,1.50029 l 1.99751,13.05863 -1.50028,0 -28.28863,0 -1.50028,0 c 0.66583,-4.35288 1.33166,-8.70576 
1.99751,-13.05863 0.12564,-0.82162 0.69519,-1.70681 1.50029,-1.50029 z"
+         id="path3688"
+         
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.60000002;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:nodetypes="sssccccss"
+         inkscape:connector-curvature="0" />
+      <g
+         id="g3713"
+         transform="matrix(1.359752,0,0,1.359752,418.09336,-671.08525)">
+        <path
+           d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 
0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 
0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 
-4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z"
+           id="path3715"
+           
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale
 Mono';-inkscape-font-specification:'Andale 
Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
+           inkscape:connector-curvature="0" />
+        <rect
+           height="11.999745"
+           id="rect3717"
+           rx="0"
+           ry="0"
+           
style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+           transform="scale(-1,1)"
+           width="2.1817718"
+           x="-102.34102"
+           y="708.92743" />
+      </g>
+      <g
+         id="g3740"
+         transform="matrix(1.359752,0,0,1.359752,492.12198,-661.29504)">
+        <path
+           d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 
0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 
0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 
-4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z"
+           id="path3742"
+           
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale
 Mono';-inkscape-font-specification:'Andale 
Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
+           inkscape:connector-curvature="0" />
+        <rect
+           height="11.999745"
+           id="rect3744"
+           rx="0"
+           ry="0"
+           
style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+           transform="scale(-1,1)"
+           width="2.1817718"
+           x="-102.34102"
+           y="708.92743" />
+      </g>
+      <g
+         id="g3746"
+         transform="matrix(1.359752,0,0,1.359752,593.58161,-661.29504)">
+        <path
+           d="m 95.250257,715.10933 0,1.09089 c -1.31e-4,0.0113 -5.02e-4,0.0227 0,0.0341 0.01222,0.27812 
0.140266,0.55621 0.340902,0.74999 l 5.693061,5.76124 5.65897,-5.76124 c 0.20529,-0.20532 0.30681,-0.49473 
0.30681,-0.78413 l 0,-1.09089 -1.09088,0 c -0.28941,0 -0.57881,0.10156 -0.78408,0.30681 l -4.09082,4.15901 
-4.124913,-4.15901 c -0.212319,-0.22989 -0.511898,-0.33071 -0.818164,-0.30681 l -1.090886,0 z"
+           id="path3748"
+           
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Andale
 Mono';-inkscape-font-specification:'Andale 
Mono';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.78124988;marker:none;enable-background:new"
+           inkscape:connector-curvature="0" />
+        <rect
+           height="11.999745"
+           id="rect3750"
+           rx="0"
+           ry="0"
+           
style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#000100;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
+           transform="scale(-1,1)"
+           width="2.1817718"
+           x="-102.34102"
+           y="708.92743" />
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/libide/shortcuts/ide-shortcut-accel-dialog.c b/libide/shortcuts/ide-shortcut-accel-dialog.c
new file mode 100644
index 0000000..e65bbd8
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-accel-dialog.c
@@ -0,0 +1,528 @@
+/* ide-shortcut-accel-dialog.c
+ *
+ * Copyright (C) 2016 Endless, Inc
+ *           (C) 2017 Christian Hergert
+ *
+ * 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 2 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/>.
+ *
+ * Authors: Georges Basile Stavracas Neto <georges stavracas gmail com>
+ *          Christian Hergert <chergert redhat com>
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-accel-dialog"
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcut-accel-dialog.h"
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-label.h"
+
+struct _IdeShortcutAccelDialog
+{
+  GtkDialog             parent_instance;
+
+  GtkStack             *stack;
+  GtkLabel             *display_label;
+  IdeShortcutLabel     *display_shortcut;
+  GtkLabel             *selection_label;
+  GtkButton            *button_cancel;
+  GtkButton            *button_set;
+
+  GdkDevice            *grab_pointer;
+
+  gchar                *shortcut_title;
+  IdeShortcutChord     *chord;
+
+  gulong                grab_source;
+
+  guint                 first_modifier;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCELERATOR,
+  PROP_SHORTCUT_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeShortcutAccelDialog, ide_shortcut_accel_dialog, GTK_TYPE_DIALOG)
+
+static GParamSpec *properties [N_PROPS];
+
+/*
+ * ide_shortcut_accel_dialog_begin_grab:
+ *
+ * This function returns %G_SOURCE_REMOVE so that it may be used as
+ * a GSourceFunc when necessary.
+ *
+ * Returns: %G_SOURCE_REMOVE always.
+ */
+static gboolean
+ide_shortcut_accel_dialog_begin_grab (IdeShortcutAccelDialog *self)
+{
+  g_autoptr(GList) seats = NULL;
+  GdkWindow *window;
+  GdkDisplay *display;
+  GdkSeat *first_seat;
+  GdkDevice *device;
+  GdkDevice *pointer;
+  GdkGrabStatus status;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  self->grab_source = 0;
+
+  if (!gtk_widget_get_mapped (GTK_WIDGET (self)))
+    return G_SOURCE_REMOVE;
+
+  if (NULL == (window = gtk_widget_get_window (GTK_WIDGET (self))))
+    return G_SOURCE_REMOVE;
+
+  display = gtk_widget_get_display (GTK_WIDGET (self));
+
+  if (NULL == (seats = gdk_display_list_seats (display)))
+    return G_SOURCE_REMOVE;
+
+  first_seat = seats->data;
+  device = gdk_seat_get_keyboard (first_seat);
+
+  if (device == NULL)
+    {
+      g_warning ("Keyboard grab unsuccessful, no keyboard in seat");
+      return G_SOURCE_REMOVE;
+    }
+
+  if (gdk_device_get_source (device) == GDK_SOURCE_KEYBOARD)
+    pointer = gdk_device_get_associated_device (device);
+  else
+    pointer = device;
+
+  status = gdk_seat_grab (gdk_device_get_seat (pointer),
+                          window,
+                          GDK_SEAT_CAPABILITY_KEYBOARD,
+                          FALSE,
+                          NULL,
+                          NULL,
+                          NULL,
+                          NULL);
+
+  if (status != GDK_GRAB_SUCCESS)
+    return G_SOURCE_REMOVE;
+
+  self->grab_pointer = pointer;
+
+  g_debug ("Grab started on %s with device %s",
+           G_OBJECT_TYPE_NAME (self),
+           G_OBJECT_TYPE_NAME (device));
+
+  gtk_grab_add (GTK_WIDGET (self));
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_shortcut_accel_dialog_release_grab (IdeShortcutAccelDialog *self)
+{
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  if (self->grab_pointer != NULL)
+    {
+      gdk_seat_ungrab (gdk_device_get_seat (self->grab_pointer));
+      self->grab_pointer = NULL;
+      gtk_grab_remove (GTK_WIDGET (self));
+    }
+}
+
+static void
+ide_shortcut_accel_dialog_map (GtkWidget *widget)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)widget;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  GTK_WIDGET_CLASS (ide_shortcut_accel_dialog_parent_class)->map (widget);
+
+  self->grab_source =
+    g_timeout_add_full (G_PRIORITY_LOW,
+                        100,
+                        (GSourceFunc) ide_shortcut_accel_dialog_begin_grab,
+                        g_object_ref (self),
+                        g_object_unref);
+}
+
+static void
+ide_shortcut_accel_dialog_unmap (GtkWidget *widget)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)widget;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  ide_shortcut_accel_dialog_release_grab (self);
+
+  GTK_WIDGET_CLASS (ide_shortcut_accel_dialog_parent_class)->unmap (widget);
+}
+
+static gboolean
+ide_shortcut_accel_dialog_is_editing (IdeShortcutAccelDialog *self)
+{
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  return self->grab_pointer != NULL;
+}
+
+static void
+ide_shortcut_accel_dialog_apply_state (IdeShortcutAccelDialog *self)
+{
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  if (self->chord != NULL)
+    {
+      gtk_stack_set_visible_child_name (self->stack, "display");
+      gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, TRUE);
+    }
+  else
+    {
+      gtk_stack_set_visible_child_name (self->stack, "selection");
+      gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, FALSE);
+    }
+}
+
+static gboolean
+ide_shortcut_accel_dialog_key_press_event (GtkWidget   *widget,
+                                           GdkEventKey *key)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)widget;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+  g_assert (key != NULL);
+
+  if (ide_shortcut_accel_dialog_is_editing (self))
+    {
+      GdkModifierType real_mask;
+      guint keyval_lower;
+
+      if (key->is_modifier)
+        {
+          /*
+           * If we are just starting a chord, we need to stash the modifier
+           * so that we know when we have finished the sequence.
+           */
+          if (self->chord == NULL && self->first_modifier == 0)
+            self->first_modifier = key->keyval;
+
+          goto chain_up;
+        }
+
+      real_mask = key->state & gtk_accelerator_get_default_mod_mask ();
+      keyval_lower = gdk_keyval_to_lower (key->keyval);
+
+      /* Normalize <Tab> */
+      if (keyval_lower == GDK_KEY_ISO_Left_Tab)
+        keyval_lower = GDK_KEY_Tab;
+
+      /* Put shift back if it changed the case of the key */
+      if (keyval_lower != key->keyval)
+        real_mask |= GDK_SHIFT_MASK;
+
+      /* We don't want to use SysRq as a keybinding but we do
+       * want Alt+Print), so we avoid translation from Alt+Print to SysRq
+       */
+      if (keyval_lower == GDK_KEY_Sys_Req && (real_mask & GDK_MOD1_MASK) != 0)
+        keyval_lower = GDK_KEY_Print;
+
+      /* A single Escape press cancels the editing */
+      if (!key->is_modifier && real_mask == 0 && keyval_lower == GDK_KEY_Escape)
+        {
+          ide_shortcut_accel_dialog_release_grab (self);
+          gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_CANCEL);
+          return GDK_EVENT_STOP;
+        }
+
+      /* Backspace disables the current shortcut */
+      if (real_mask == 0 && keyval_lower == GDK_KEY_BackSpace)
+        {
+          ide_shortcut_accel_dialog_set_accelerator (self, NULL);
+          gtk_dialog_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT);
+          return GDK_EVENT_STOP;
+        }
+
+      if (self->chord == NULL)
+        self->chord = ide_shortcut_chord_new_from_event (key);
+      else
+        ide_shortcut_chord_append_event (self->chord, key);
+
+      ide_shortcut_accel_dialog_apply_state (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]);
+
+      return GDK_EVENT_STOP;
+    }
+
+chain_up:
+  return GTK_WIDGET_CLASS (ide_shortcut_accel_dialog_parent_class)->key_press_event (widget, key);
+}
+
+static gboolean
+ide_shortcut_accel_dialog_key_release_event (GtkWidget   *widget,
+                                             GdkEventKey *key)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)widget;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+  g_assert (key != NULL);
+
+  if (self->chord != NULL)
+    {
+      /*
+       * If we have a chord defined and there was no modifier,
+       * then any key release should be enough for us to cancel
+       * our grab.
+       */
+      if (!ide_shortcut_chord_has_modifier (self->chord))
+        {
+          ide_shortcut_accel_dialog_release_grab (self);
+          goto chain_up;
+        }
+
+      /*
+       * If we started our sequence with a modifier, we want to
+       * release our grab when that modifier has been released.
+       */
+      if (key->is_modifier &&
+          self->first_modifier != 0 &&
+          self->first_modifier == key->keyval)
+        {
+          self->first_modifier = 0;
+          ide_shortcut_accel_dialog_release_grab (self);
+          goto chain_up;
+        }
+    }
+
+  /* Clear modifier if it was released before a chord was made */
+  if (self->first_modifier == key->keyval)
+    self->first_modifier = 0;
+
+chain_up:
+  return GTK_WIDGET_CLASS (ide_shortcut_accel_dialog_parent_class)->key_release_event (widget, key);
+}
+
+static void
+ide_shortcut_accel_dialog_destroy (GtkWidget *widget)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)widget;
+
+  g_assert (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  if (self->grab_source != 0)
+    {
+      g_source_remove (self->grab_source);
+      self->grab_source = 0;
+    }
+
+  GTK_WIDGET_CLASS (ide_shortcut_accel_dialog_parent_class)->destroy (widget);
+}
+
+static void
+ide_shortcut_accel_dialog_finalize (GObject *object)
+{
+  IdeShortcutAccelDialog *self = (IdeShortcutAccelDialog *)object;
+
+  g_clear_pointer (&self->shortcut_title, g_free);
+  g_clear_pointer (&self->chord, ide_shortcut_chord_free);
+
+  G_OBJECT_CLASS (ide_shortcut_accel_dialog_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_accel_dialog_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeShortcutAccelDialog *self = IDE_SHORTCUT_ACCEL_DIALOG (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      g_value_take_string (value, ide_shortcut_accel_dialog_get_accelerator (self));
+      break;
+
+    case PROP_SHORTCUT_TITLE:
+      g_value_set_string (value, ide_shortcut_accel_dialog_get_shortcut_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_accel_dialog_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeShortcutAccelDialog *self = IDE_SHORTCUT_ACCEL_DIALOG (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      ide_shortcut_accel_dialog_set_accelerator (self, g_value_get_string (value));
+      break;
+
+    case PROP_SHORTCUT_TITLE:
+      ide_shortcut_accel_dialog_set_shortcut_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_accel_dialog_class_init (IdeShortcutAccelDialogClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_accel_dialog_finalize;
+  object_class->get_property = ide_shortcut_accel_dialog_get_property;
+  object_class->set_property = ide_shortcut_accel_dialog_set_property;
+
+  widget_class->destroy = ide_shortcut_accel_dialog_destroy;
+  widget_class->map = ide_shortcut_accel_dialog_map;
+  widget_class->unmap = ide_shortcut_accel_dialog_unmap;
+  widget_class->key_press_event = ide_shortcut_accel_dialog_key_press_event;
+  widget_class->key_release_event = ide_shortcut_accel_dialog_key_release_event;
+
+  properties [PROP_ACCELERATOR] =
+    g_param_spec_string ("accelerator",
+                         "Accelerator",
+                         "Accelerator",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHORTCUT_TITLE] =
+    g_param_spec_string ("shortcut-title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-shortcut-accel-dialog.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, selection_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, display_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, display_shortcut);
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, button_cancel);
+  gtk_widget_class_bind_template_child (widget_class, IdeShortcutAccelDialog, button_set);
+
+  g_type_ensure (IDE_TYPE_SHORTCUT_LABEL);
+}
+
+static void
+ide_shortcut_accel_dialog_init (IdeShortcutAccelDialog *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_dialog_add_buttons (GTK_DIALOG (self),
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Set"), GTK_RESPONSE_ACCEPT,
+                          NULL);
+  gtk_dialog_set_default_response (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT);
+
+  gtk_dialog_set_response_sensitive (GTK_DIALOG (self), GTK_RESPONSE_ACCEPT, FALSE);
+
+  g_object_bind_property (self, "accelerator",
+                          self->display_shortcut, "accelerator",
+                          G_BINDING_SYNC_CREATE);
+}
+
+gchar *
+ide_shortcut_accel_dialog_get_accelerator (IdeShortcutAccelDialog *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_ACCEL_DIALOG (self), NULL);
+
+  if (self->chord == NULL)
+    return NULL;
+
+  return ide_shortcut_chord_to_string (self->chord);
+}
+
+void
+ide_shortcut_accel_dialog_set_accelerator (IdeShortcutAccelDialog *self,
+                                           const gchar            *accelerator)
+{
+  g_autoptr(IdeShortcutChord) chord = NULL;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  if (accelerator)
+    chord = ide_shortcut_chord_new_from_string (accelerator);
+
+  if (!ide_shortcut_chord_equal (chord, self->chord))
+    {
+      ide_shortcut_chord_free (self->chord);
+      self->chord = g_steal_pointer (&chord);
+      gtk_dialog_set_response_sensitive (GTK_DIALOG (self),
+                                         GTK_RESPONSE_ACCEPT,
+                                         self->chord != NULL);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]);
+    }
+}
+
+void
+ide_shortcut_accel_dialog_set_shortcut_title (IdeShortcutAccelDialog *self,
+                                              const gchar            *shortcut_title)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_ACCEL_DIALOG (self));
+
+  if (g_strcmp0 (shortcut_title, self->shortcut_title) != 0)
+    {
+      g_autofree gchar *label = NULL;
+
+      if (shortcut_title != NULL)
+        {
+          /* Translators: <b>%s</b> is used to show the provided text in bold */
+          label = g_strdup_printf (_("Enter new shortcut to change <b>%s</b>."), shortcut_title);
+        }
+
+      gtk_label_set_label (self->selection_label, label);
+      gtk_label_set_label (self->display_label, label);
+
+      g_free (self->shortcut_title);
+      self->shortcut_title = g_strdup (shortcut_title);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHORTCUT_TITLE]);
+    }
+}
+
+const gchar *
+ide_shortcut_accel_dialog_get_shortcut_title (IdeShortcutAccelDialog *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_ACCEL_DIALOG (self), NULL);
+
+  return self->shortcut_title;
+}
+
+const IdeShortcutChord *
+ide_shortcut_accel_dialog_get_chord (IdeShortcutAccelDialog *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_ACCEL_DIALOG (self), NULL);
+
+  return self->chord;
+}
diff --git a/libide/shortcuts/ide-shortcut-accel-dialog.h b/libide/shortcuts/ide-shortcut-accel-dialog.h
new file mode 100644
index 0000000..8023e2c
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-accel-dialog.h
@@ -0,0 +1,43 @@
+/* ide-shortcut-accel-dialog.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_ACCEL_DIALOG_H
+#define IDE_SHORTCUT_ACCEL_DIALOG_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-chord.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_ACCEL_DIALOG (ide_shortcut_accel_dialog_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutAccelDialog, ide_shortcut_accel_dialog, IDE, SHORTCUT_ACCEL_DIALOG, 
GtkDialog)
+
+GtkWidget              *ide_shortcut_accel_dialog_new                (void);
+gchar                  *ide_shortcut_accel_dialog_get_accelerator    (IdeShortcutAccelDialog *self);
+void                    ide_shortcut_accel_dialog_set_accelerator    (IdeShortcutAccelDialog *self,
+                                                                      const gchar            *accelerator);
+const IdeShortcutChord *ide_shortcut_accel_dialog_get_chord          (IdeShortcutAccelDialog *self);
+const gchar            *ide_shortcut_accel_dialog_get_shortcut_title (IdeShortcutAccelDialog *self);
+void                    ide_shortcut_accel_dialog_set_shortcut_title (IdeShortcutAccelDialog *self,
+                                                                      const gchar            *title);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_ACCEL_DIALOG_H */
diff --git a/libide/shortcuts/ide-shortcut-accel-dialog.ui b/libide/shortcuts/ide-shortcut-accel-dialog.ui
new file mode 100644
index 0000000..121b181
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-accel-dialog.ui
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<interface>
+  <template class="GseShortcutAccelDialog" parent="GtkDialog">
+    <child type="action">
+      <object class="GtkButton" id="button_cancel">
+        <property name="can-default">true</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="button_set">
+        <property name="can-default">true</property>
+      </object>
+    </child>
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="homogeneous">true</property>
+            <property name="margin">24</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="spacing">18</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel" id="selection_label">
+                    <property name="xalign">0.5</property>
+                    <property name="use-markup">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="resource">/org/gnome/gse/icons/enter-keyboard-shortcut.svg</property>
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="xalign">0.5</property>
+                    <property name="label" translatable="yes">Press Escape to cancel or Backspace to 
disable.</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">selection</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="spacing">18</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel" id="display_label">
+                    <property name="xalign">0.5</property>
+                    <property name="use-markup">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GseShortcutLabel" id="display_shortcut">
+                    <property name="halign">center</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">display</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="accept">button_set</action-widget>
+      <action-widget response="cancel">button_cancel</action-widget>
+    </action-widgets>
+  </template>
+</interface>
diff --git a/libide/shortcuts/ide-shortcut-chord.c b/libide/shortcuts/ide-shortcut-chord.c
new file mode 100644
index 0000000..9bd082b
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-chord.c
@@ -0,0 +1,630 @@
+/* ide-shortcut-chord.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-chord"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-private.h"
+
+#define MAX_CHORD_SIZE 4
+
+G_DEFINE_BOXED_TYPE (IdeShortcutChord, ide_shortcut_chord,
+                     ide_shortcut_chord_copy, ide_shortcut_chord_free)
+G_DEFINE_POINTER_TYPE (IdeShortcutChordTable, ide_shortcut_chord_table)
+
+typedef struct
+{
+  guint           keyval;
+  GdkModifierType modifier;
+} IdeShortcutKey;
+
+struct _IdeShortcutChord
+{
+  IdeShortcutKey keys[MAX_CHORD_SIZE];
+};
+
+typedef struct
+{
+  IdeShortcutChord chord;
+  gpointer data;
+} IdeShortcutChordTableEntry;
+
+struct _IdeShortcutChordTable
+{
+  IdeShortcutChordTableEntry *entries;
+  GDestroyNotify              destroy;
+  guint                       len;
+  guint                       size;
+};
+
+static GdkModifierType
+sanitize_modifier_mask (GdkModifierType mods)
+{
+  mods &= gtk_accelerator_get_default_mod_mask ();
+  mods &= ~GDK_LOCK_MASK;
+
+  return mods;
+}
+
+static gint
+ide_shortcut_chord_compare (const IdeShortcutChord *a,
+                            const IdeShortcutChord *b)
+{
+  return memcmp (a, b, sizeof *a);
+}
+
+static gboolean
+ide_shortcut_chord_is_valid (IdeShortcutChord *self)
+{
+  g_assert (self != NULL);
+
+  /* Ensure we got a valid first key at least */
+  if (self->keys[0].keyval == 0)
+    return FALSE;
+
+  return TRUE;
+}
+
+IdeShortcutChord *
+ide_shortcut_chord_new_from_event (const GdkEventKey *key)
+{
+  IdeShortcutChord *self;
+
+  g_return_val_if_fail (key != NULL, NULL);
+
+  /* Ignore modifier keypresses */
+  if (key->is_modifier)
+    return NULL;
+
+  self = g_slice_new0 (IdeShortcutChord);
+
+  self->keys[0].keyval = gdk_keyval_to_lower (key->keyval);
+  self->keys[0].modifier = sanitize_modifier_mask (key->state);
+
+  if (self->keys[0].keyval != key->keyval)
+    self->keys[0].modifier |= GDK_SHIFT_MASK;
+
+  if (!ide_shortcut_chord_is_valid (self))
+    g_clear_pointer (&self, ide_shortcut_chord_free);
+
+  return self;
+}
+
+IdeShortcutChord *
+ide_shortcut_chord_new_from_string (const gchar *accelerator)
+{
+  IdeShortcutChord *self;
+  g_auto(GStrv) parts = NULL;
+
+  g_return_val_if_fail (accelerator != NULL, NULL);
+
+  /* We might have a single key, or chord defined */
+  parts = g_strsplit (accelerator, "|", 0);
+
+  /* Make sure we won't overflow the keys array */
+  if (g_strv_length (parts) > G_N_ELEMENTS (self->keys))
+    return NULL;
+
+  self = g_slice_new0 (IdeShortcutChord);
+
+  /* Parse each section from the accelerator */
+  for (guint i = 0; parts[i]; i++)
+    gtk_accelerator_parse (parts[i], &self->keys[i].keyval, &self->keys[i].modifier);
+
+  /* Ensure we got a valid first key at least */
+  if (!ide_shortcut_chord_is_valid (self))
+    g_clear_pointer (&self, ide_shortcut_chord_free);
+
+  return self;
+}
+
+gboolean
+ide_shortcut_chord_append_event (IdeShortcutChord  *self,
+                                 const GdkEventKey *key)
+{
+  guint i;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  for (i = 0; i < G_N_ELEMENTS (self->keys); i++)
+    {
+      if (self->keys[i].keyval == 0)
+        {
+          self->keys[i].keyval = gdk_keyval_to_lower (key->keyval);
+          self->keys[i].modifier = sanitize_modifier_mask (key->state);
+
+          if (self->keys[i].keyval != key->keyval)
+            self->keys[i].modifier |= GDK_SHIFT_MASK;
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static inline gboolean
+ide_shortcut_key_equal (const IdeShortcutKey *keya,
+                        const IdeShortcutKey *keyb)
+{
+  if (keya == keyb)
+    return TRUE;
+  else if (keya == NULL || keyb == NULL)
+    return FALSE;
+
+  return memcmp (keya, keyb, sizeof *keya) == 0;
+}
+
+static inline guint
+ide_shortcut_chord_count_keys (const IdeShortcutChord *self)
+{
+  guint count = 0;
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
+    {
+      if (self->keys[i].keyval != 0)
+        count++;
+      else
+        break;
+    }
+
+  return count;
+}
+
+IdeShortcutMatch
+ide_shortcut_chord_match (const IdeShortcutChord *self,
+                          const IdeShortcutChord *other)
+{
+  guint self_count = 0;
+  guint other_count = 0;
+
+  g_return_val_if_fail (self != NULL, IDE_SHORTCUT_MATCH_NONE);
+  g_return_val_if_fail (other != NULL, IDE_SHORTCUT_MATCH_NONE);
+
+  self_count = ide_shortcut_chord_count_keys (self);
+  other_count = ide_shortcut_chord_count_keys (other);
+
+  if (self_count > other_count)
+    return IDE_SHORTCUT_MATCH_NONE;
+
+  if (0 == memcmp (self->keys, other->keys, sizeof (IdeShortcutKey) * self_count))
+    return self_count == other_count ? IDE_SHORTCUT_MATCH_EQUAL : IDE_SHORTCUT_MATCH_PARTIAL;
+
+  return IDE_SHORTCUT_MATCH_NONE;
+}
+
+gchar *
+ide_shortcut_chord_to_string (const IdeShortcutChord *self)
+{
+  GString *str;
+
+  if (self == NULL || self->keys[0].keyval == 0)
+    return NULL;
+
+  str = g_string_new (NULL);
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
+    {
+      const IdeShortcutKey *key = &self->keys[i];
+      g_autofree gchar *name = NULL;
+
+      if (key->keyval == 0 && key->modifier == 0)
+        break;
+
+      name = gtk_accelerator_name (key->keyval, key->modifier);
+
+      if (i != 0)
+        g_string_append_c (str, '|');
+
+      g_string_append (str, name);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+gchar *
+ide_shortcut_chord_get_label (const IdeShortcutChord *self)
+{
+  GString *str;
+
+  if (self == NULL || self->keys[0].keyval == 0)
+    return NULL;
+
+  str = g_string_new (NULL);
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
+    {
+      const IdeShortcutKey *key = &self->keys[i];
+      g_autofree gchar *name = NULL;
+
+      if (key->keyval == 0 && key->modifier == 0)
+        break;
+
+      name = gtk_accelerator_get_label (key->keyval, key->modifier);
+
+      if (i != 0)
+        g_string_append_c (str, ' ');
+
+      g_string_append (str, name);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+IdeShortcutChord *
+ide_shortcut_chord_copy (const IdeShortcutChord *self)
+{
+  IdeShortcutChord *copy;
+
+  if (self == NULL)
+    return NULL;
+
+  copy = g_slice_new (IdeShortcutChord);
+  memcpy (copy, self, sizeof *copy);
+
+  return copy;
+}
+
+guint
+ide_shortcut_chord_hash (gconstpointer data)
+{
+  const IdeShortcutChord *self = data;
+  guint hash = 0;
+
+  for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
+    {
+      const IdeShortcutKey *key = &self->keys[i];
+
+      hash ^= key->keyval;
+      hash ^= key->modifier;
+    }
+
+  return hash;
+}
+
+gboolean
+ide_shortcut_chord_equal (gconstpointer data1,
+                          gconstpointer data2)
+{
+  if (data1 == data2)
+    return TRUE;
+  else if (data1 == NULL || data2 == NULL)
+    return FALSE;
+
+  return 0 == memcmp (((const IdeShortcutChord *)data1)->keys,
+                      ((const IdeShortcutChord *)data2)->keys,
+                      sizeof (IdeShortcutChord));
+}
+
+void
+ide_shortcut_chord_free (IdeShortcutChord *self)
+{
+  if (self != NULL)
+    g_slice_free (IdeShortcutChord, self);
+}
+
+GType
+ide_shortcut_match_get_type (void)
+{
+  static GType type_id;
+
+  if (g_once_init_enter (&type_id))
+    {
+      static GEnumValue values[] = {
+        { IDE_SHORTCUT_MATCH_NONE, "IDE_SHORTCUT_MATCH_NONE", "none" },
+        { IDE_SHORTCUT_MATCH_EQUAL, "IDE_SHORTCUT_MATCH_EQUAL", "equal" },
+        { IDE_SHORTCUT_MATCH_PARTIAL, "IDE_SHORTCUT_MATCH_PARTIAL", "partial" },
+        { 0 }
+      };
+      GType _type_id = g_enum_register_static ("IdeShortcutMatch", values);
+      g_once_init_leave (&type_id, _type_id);
+    }
+
+  return type_id;
+}
+
+static gint
+ide_shortcut_chord_table_sort (gconstpointer a,
+                               gconstpointer b)
+{
+  const IdeShortcutChordTableEntry *keya = a;
+  const IdeShortcutChordTableEntry *keyb = b;
+
+  return ide_shortcut_chord_compare (&keya->chord, &keyb->chord);
+}
+
+/**
+ * ide_shortcut_chord_table_new: (skip)
+ */
+IdeShortcutChordTable *
+ide_shortcut_chord_table_new (void)
+{
+  IdeShortcutChordTable *table;
+
+  table = g_slice_new0 (IdeShortcutChordTable);
+  table->len = 0;
+  table->size = 4;
+  table->destroy = NULL;
+  table->entries = g_new0 (IdeShortcutChordTableEntry, table->size);
+
+  return table;
+}
+
+void
+ide_shortcut_chord_table_free (IdeShortcutChordTable *self)
+{
+  if (self != NULL)
+    {
+      if (self->destroy != NULL)
+        {
+          for (guint i = 0; i < self->len; i++)
+            self->destroy (self->entries[i].data);
+        }
+      g_free (self->entries);
+      g_slice_free (IdeShortcutChordTable, self);
+    }
+}
+
+void
+ide_shortcut_chord_table_add (IdeShortcutChordTable  *self,
+                              const IdeShortcutChord *chord,
+                              gpointer                data)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (chord != NULL);
+
+  if (self->len == self->size)
+    {
+      self->size *= 2;
+      self->entries = g_renew (IdeShortcutChordTableEntry, self->entries, self->size);
+    }
+
+  self->entries[self->len].chord = *chord;
+  self->entries[self->len].data = data;
+
+  self->len++;
+
+  qsort (self->entries,
+         self->len,
+         sizeof (IdeShortcutChordTableEntry),
+         ide_shortcut_chord_table_sort);
+}
+
+gboolean
+ide_shortcut_chord_table_remove (IdeShortcutChordTable  *self,
+                                 const IdeShortcutChord *chord)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  for (guint i = 0; i < self->len; i++)
+    {
+      IdeShortcutChordTableEntry *ele = &self->entries[i];
+
+      if (ide_shortcut_chord_equal (&ele->chord, chord))
+        {
+          gpointer data = ele->data;
+
+          if (i + 1 < self->len)
+            memmove (ele, ele + 1, self->len - i - 1);
+
+          self->len--;
+
+          if (self->destroy != NULL)
+            self->destroy (data);
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static gint
+ide_shortcut_chord_find_partial (gconstpointer a,
+                                 gconstpointer b)
+{
+  const IdeShortcutChord *key = a;
+  const IdeShortcutChordTableEntry *element = b;
+
+  /*
+   * We are only looking for a partial match here so that we can walk backwards
+   * after the bsearch to the first partial match.
+   */
+  if (ide_shortcut_chord_match (key, &element->chord) != IDE_SHORTCUT_MATCH_NONE)
+    return 0;
+
+  return ide_shortcut_chord_compare (key, &element->chord);
+}
+
+IdeShortcutMatch
+ide_shortcut_chord_table_lookup (IdeShortcutChordTable  *self,
+                                 const IdeShortcutChord *chord,
+                                 gpointer               *data)
+{
+  const IdeShortcutChordTableEntry *match;
+
+  g_return_val_if_fail (self != NULL, IDE_SHORTCUT_MATCH_NONE);
+  g_return_val_if_fail (chord != NULL, IDE_SHORTCUT_MATCH_NONE);
+
+  if (data != NULL)
+    *data = NULL;
+
+  if (self->len == 0)
+    return IDE_SHORTCUT_MATCH_NONE;
+
+  /*
+   * This function works by performing a binary search to locate ourself
+   * somewhere within a match zone of the array. Once we are there, we walk
+   * back to the first item that is a partial match.  After that, we walk
+   * through every potential match looking for an exact match until we reach a
+   * non-partial-match or the end of the array.
+   *
+   * Based on our findings, we return the appropriate IdeShortcutMatch.
+   */
+
+  match = bsearch (chord, self->entries, self->len, sizeof (IdeShortcutChordTableEntry),
+                   ide_shortcut_chord_find_partial);
+
+  if (match != NULL)
+    {
+      const IdeShortcutChordTableEntry *begin = self->entries;
+      const IdeShortcutChordTableEntry *end = self->entries + self->len;
+      IdeShortcutMatch ret = IDE_SHORTCUT_MATCH_PARTIAL;
+
+      /* Find the first patial match */
+      while ((match - 1) >= begin &&
+             ide_shortcut_chord_match (chord, &(match - 1)->chord) != IDE_SHORTCUT_MATCH_NONE)
+        match--;
+
+      g_assert (match >= begin);
+
+      /* Now walk forward to see if we have an exact match */
+      while (IDE_SHORTCUT_MATCH_NONE != (ret = ide_shortcut_chord_match (chord, &match->chord)))
+        {
+          if (ret == IDE_SHORTCUT_MATCH_EQUAL)
+            {
+              if (data != NULL)
+                *data = match->data;
+              return IDE_SHORTCUT_MATCH_EQUAL;
+            }
+
+          match++;
+
+          g_assert (match <= end);
+
+          if (ret == 0 || match == end)
+            break;
+        }
+
+      return IDE_SHORTCUT_MATCH_PARTIAL;
+    }
+
+  return IDE_SHORTCUT_MATCH_NONE;
+}
+
+void
+ide_shortcut_chord_table_set_free_func (IdeShortcutChordTable *self,
+                                        GDestroyNotify         destroy)
+{
+  g_return_if_fail (self != NULL);
+
+  self->destroy = destroy;
+}
+
+guint
+ide_shortcut_chord_table_size (const IdeShortcutChordTable *self)
+{
+  return self ? self->len : 0;
+}
+
+void
+ide_shortcut_chord_table_printf (const IdeShortcutChordTable *self)
+{
+  if (self == NULL)
+    return;
+
+  for (guint i = 0; i < self->len; i++)
+    {
+      const IdeShortcutChordTableEntry *entry = &self->entries[i];
+      g_autofree gchar *str = ide_shortcut_chord_to_string (&entry->chord);
+
+      g_print ("%s\n", str);
+    }
+}
+
+void
+_ide_shortcut_chord_table_iter_init (IdeShortcutChordTableIter *iter,
+                                     IdeShortcutChordTable     *table)
+{
+  g_return_if_fail (iter != NULL);
+
+  iter->table = table;
+  iter->position = 0;
+}
+
+gboolean
+_ide_shortcut_chord_table_iter_next (IdeShortcutChordTableIter  *iter,
+                                     const IdeShortcutChord    **chord,
+                                     gpointer                   *value)
+{
+  g_return_val_if_fail (iter != NULL, FALSE);
+
+  if (iter->table == NULL)
+    return FALSE;
+
+  if (iter->position < iter->table->len)
+    {
+      *chord = &iter->table->entries[iter->position].chord;
+      *value = iter->table->entries[iter->position].data;
+      iter->position++;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_shortcut_chord_has_modifier (const IdeShortcutChord *self)
+{
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  return self->keys[0].modifier != 0;
+}
+
+guint
+ide_shortcut_chord_get_length (const IdeShortcutChord *self)
+{
+  if (self != NULL)
+    {
+      for (guint i = 0; i < G_N_ELEMENTS (self->keys); i++)
+        {
+          if (self->keys[i].keyval == 0)
+            return i;
+        }
+
+      return G_N_ELEMENTS (self->keys);
+    }
+
+  return 0;
+}
+
+void
+ide_shortcut_chord_get_nth_key (const IdeShortcutChord *self,
+                                guint                   nth,
+                                guint                  *keyval,
+                                GdkModifierType        *modifier)
+{
+  if (nth < G_N_ELEMENTS (self->keys))
+    {
+      if (keyval)
+        *keyval = self->keys[nth].keyval;
+      if (modifier)
+        *modifier = self->keys[nth].modifier;
+    }
+  else
+    {
+      if (keyval)
+        *keyval = 0;
+      if (modifier)
+        *modifier = 0;
+    }
+}
diff --git a/libide/shortcuts/ide-shortcut-chord.h b/libide/shortcuts/ide-shortcut-chord.h
new file mode 100644
index 0000000..d3c7a59
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-chord.h
@@ -0,0 +1,81 @@
+/* ide-shortcut-chord.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_CHORD_H
+#define IDE_SHORTCUT_CHORD_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_SHORTCUT_MATCH_NONE,
+  IDE_SHORTCUT_MATCH_EQUAL,
+  IDE_SHORTCUT_MATCH_PARTIAL
+} IdeShortcutMatch;
+
+#define IDE_TYPE_SHORTCUT_CHORD       (ide_shortcut_chord_get_type())
+#define IDE_TYPE_SHORTCUT_CHORD_TABLE (ide_shortcut_chord_table_get_type())
+#define IDE_TYPE_SHORTCUT_MATCH       (ide_shortcut_match_get_type())
+
+typedef struct _IdeShortcutChord      IdeShortcutChord;
+typedef struct _IdeShortcutChordTable IdeShortcutChordTable;
+
+GType                  ide_shortcut_chord_get_type            (void);
+IdeShortcutChord      *ide_shortcut_chord_new_from_event      (const GdkEventKey           *event);
+IdeShortcutChord      *ide_shortcut_chord_new_from_string     (const gchar                 *accelerator);
+gchar                 *ide_shortcut_chord_to_string           (const IdeShortcutChord      *self);
+gchar                 *ide_shortcut_chord_get_label           (const IdeShortcutChord      *self);
+guint                  ide_shortcut_chord_get_length          (const IdeShortcutChord      *self);
+void                   ide_shortcut_chord_get_nth_key         (const IdeShortcutChord      *self,
+                                                               guint                        nth,
+                                                               guint                       *keyval,
+                                                               GdkModifierType             *modifier);
+gboolean               ide_shortcut_chord_has_modifier        (const IdeShortcutChord      *self);
+gboolean               ide_shortcut_chord_append_event        (IdeShortcutChord            *self,
+                                                               const GdkEventKey           *event);
+IdeShortcutMatch       ide_shortcut_chord_match               (const IdeShortcutChord      *self,
+                                                               const IdeShortcutChord      *other);
+guint                  ide_shortcut_chord_hash                (gconstpointer                data);
+gboolean               ide_shortcut_chord_equal               (gconstpointer                data1,
+                                                               gconstpointer                data2);
+IdeShortcutChord      *ide_shortcut_chord_copy                (const IdeShortcutChord      *self);
+void                   ide_shortcut_chord_free                (IdeShortcutChord            *self);
+GType                  ide_shortcut_chord_table_get_type      (void);
+IdeShortcutChordTable *ide_shortcut_chord_table_new           (void);
+void                   ide_shortcut_chord_table_set_free_func (IdeShortcutChordTable       *self,
+                                                               GDestroyNotify               notify);
+void                   ide_shortcut_chord_table_free          (IdeShortcutChordTable       *self);
+void                   ide_shortcut_chord_table_add           (IdeShortcutChordTable       *self,
+                                                               const IdeShortcutChord      *chord,
+                                                               gpointer                     data);
+gboolean               ide_shortcut_chord_table_remove        (IdeShortcutChordTable       *self,
+                                                               const IdeShortcutChord      *chord);
+IdeShortcutMatch       ide_shortcut_chord_table_lookup        (IdeShortcutChordTable       *self,
+                                                               const IdeShortcutChord      *chord,
+                                                               gpointer                    *data);
+guint                  ide_shortcut_chord_table_size          (const IdeShortcutChordTable *self);
+void                   ide_shortcut_chord_table_printf        (const IdeShortcutChordTable *self);
+GType                  ide_shortcut_match_get_type            (void);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeShortcutChord, ide_shortcut_chord_free)
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CHORD_H */
diff --git a/libide/shortcuts/ide-shortcut-context.c b/libide/shortcuts/ide-shortcut-context.c
new file mode 100644
index 0000000..6f52ec2
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.c
@@ -0,0 +1,751 @@
+/* ide-shortcut-context.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-context"
+
+#include <gobject/gvaluecollector.h>
+#include <string.h>
+
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-context.h"
+#include "ide-shortcut-controller.h"
+#include "ide-shortcut-private.h"
+
+typedef struct
+{
+  gchar                 *name;
+  IdeShortcutChordTable *table;
+  guint                  use_binding_sets : 1;
+} IdeShortcutContextPrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_USE_BINDING_SETS,
+  N_PROPS
+};
+
+struct _IdeShortcutContext { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutContext, ide_shortcut_context, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+shortcut_free (gpointer data)
+{
+  Shortcut *shortcut = data;
+
+  if (shortcut != NULL)
+    {
+      g_clear_pointer (&shortcut->next, shortcut_free);
+
+      switch (shortcut->type)
+        {
+        case SHORTCUT_ACTION:
+          g_clear_pointer (&shortcut->action.param, g_variant_unref);
+          break;
+
+        case SHORTCUT_SIGNAL:
+          g_array_unref (shortcut->signal.params);
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+
+      g_slice_free (Shortcut, shortcut);
+    }
+}
+
+static gboolean
+widget_action (GtkWidget   *widget,
+               const gchar *prefix,
+               const gchar *action_name,
+               GVariant    *parameter)
+{
+  GtkWidget *toplevel;
+  GApplication *app;
+  GActionGroup *group = NULL;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (prefix != NULL);
+  g_assert (action_name != NULL);
+
+  app = g_application_get_default ();
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  while ((group == NULL) && (widget != NULL))
+    {
+      group = gtk_widget_get_action_group (widget, prefix);
+
+      if G_UNLIKELY (GTK_IS_POPOVER (widget))
+        {
+          GtkWidget *relative_to;
+
+          relative_to = gtk_popover_get_relative_to (GTK_POPOVER (widget));
+
+          if (relative_to != NULL)
+            widget = relative_to;
+          else
+            widget = gtk_widget_get_parent (widget);
+        }
+      else
+        {
+          widget = gtk_widget_get_parent (widget);
+        }
+    }
+
+  if (!group && g_str_equal (prefix, "win") && G_IS_ACTION_GROUP (toplevel))
+    group = G_ACTION_GROUP (toplevel);
+
+  if (!group && g_str_equal (prefix, "app") && G_IS_ACTION_GROUP (app))
+    group = G_ACTION_GROUP (app);
+
+  if (group && g_action_group_has_action (group, action_name))
+    {
+      g_action_group_activate_action (group, action_name, parameter);
+      return TRUE;
+    }
+
+  g_warning ("Failed to locate action %s.%s", prefix, action_name);
+
+  return FALSE;
+}
+
+static gboolean
+shortcut_action_activate (Shortcut  *shortcut,
+                          GtkWidget *widget)
+{
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+
+  return widget_action (widget,
+                        shortcut->action.prefix,
+                        shortcut->action.name,
+                        shortcut->action.param);
+}
+
+static gboolean
+find_instance_and_signal (GtkWidget          *widget,
+                          const gchar        *signal_name,
+                          gpointer           *instance,
+                          GSignalQuery       *query)
+{
+  IdeShortcutController *controller;
+
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (signal_name != NULL);
+  g_assert (instance != NULL);
+  g_assert (query != NULL);
+
+  *instance = NULL;
+
+  /*
+   * First we want to see if we can resolve the signal on the widgets
+   * controller (if there is one). This allows us to change contexts
+   * from signals without installing signals on the actual widgets.
+   */
+
+  controller = ide_shortcut_controller_find (widget);
+
+  if (controller != NULL)
+    {
+      guint signal_id;
+
+      signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (controller));
+
+      if (signal_id != 0)
+        {
+          g_signal_query (signal_id, query);
+          *instance = controller;
+          return TRUE;
+        }
+    }
+
+  /*
+   * This diverts from Gtk signal keybindings a bit in that we
+   * allow you to activate a signal on any widget in the focus
+   * hierarchy starting from the provided widget up.
+   */
+
+  while (widget != NULL)
+    {
+      guint signal_id;
+
+      signal_id = g_signal_lookup (signal_name, G_OBJECT_TYPE (widget));
+
+      if (signal_id != 0)
+        {
+          g_signal_query (signal_id, query);
+          *instance = widget;
+          return TRUE;
+        }
+
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  return FALSE;
+}
+
+static gboolean
+shortcut_signal_activate (Shortcut  *shortcut,
+                          GtkWidget *widget)
+{
+  GValue *params;
+  GValue return_value = { 0 };
+  GSignalQuery query;
+  gpointer instance = NULL;
+
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (!find_instance_and_signal (widget, shortcut->signal.name, &instance, &query))
+    {
+      g_warning ("Failed to locate signal %s in hierarchy of %s",
+                 shortcut->signal.name, G_OBJECT_TYPE_NAME (widget));
+      return TRUE;
+    }
+
+  if (query.n_params != shortcut->signal.params->len)
+    goto parameter_mismatch;
+
+  for (guint i = 0; i < query.n_params; i++)
+    {
+      if (!G_VALUE_HOLDS (&g_array_index (shortcut->signal.params, GValue, i), query.param_types[i]))
+        goto parameter_mismatch;
+    }
+
+  params = g_new0 (GValue, 1 + query.n_params);
+  g_value_init_from_instance (&params[0], instance);
+  for (guint i = 0; i < query.n_params; i++)
+    {
+      GValue *src_value = &g_array_index (shortcut->signal.params, GValue, i);
+
+      g_value_init (&params[1+i], G_VALUE_TYPE (src_value));
+      g_value_copy (src_value, &params[1+i]);
+    }
+
+  if (query.return_type != G_TYPE_NONE)
+    g_value_init (&return_value, query.return_type);
+
+  g_signal_emitv (params, query.signal_id, shortcut->signal.detail, &return_value);
+
+  for (guint i = 0; i < query.n_params + 1; i++)
+    g_value_unset (&params[i]);
+  g_free (params);
+
+  return GDK_EVENT_STOP;
+
+parameter_mismatch:
+  g_warning ("The parameters are not correct for signal %s",
+             shortcut->signal.name);
+
+  /*
+   * If there was a bug with the signal descriptor, we still want
+   * to swallow the event to keep it from propagating further.
+   */
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+shortcut_activate (Shortcut  *shortcut,
+                   GtkWidget *widget)
+{
+  gboolean handled = FALSE;
+
+  g_assert (shortcut != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+
+  for (; shortcut != NULL; shortcut = shortcut->next)
+    {
+      switch (shortcut->type)
+        {
+        case SHORTCUT_ACTION:
+          handled |= shortcut_action_activate (shortcut, widget);
+          break;
+
+        case SHORTCUT_SIGNAL:
+          handled |= shortcut_signal_activate (shortcut, widget);
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+    }
+
+  return handled;
+}
+
+static void
+ide_shortcut_context_finalize (GObject *object)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->table, ide_shortcut_chord_table_free);
+
+  G_OBJECT_CLASS (ide_shortcut_context_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_context_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, priv->name);
+      break;
+
+    case PROP_USE_BINDING_SETS:
+      g_value_set_boolean (value, priv->use_binding_sets);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_context_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  IdeShortcutContext *self = (IdeShortcutContext *)object;
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_USE_BINDING_SETS:
+      priv->use_binding_sets = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_context_class_init (IdeShortcutContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_context_finalize;
+  object_class->get_property = ide_shortcut_context_get_property;
+  object_class->set_property = ide_shortcut_context_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_BINDING_SETS] =
+    g_param_spec_boolean ("use-binding-sets",
+                          "Use Binding Sets",
+                          "If the context should allow activation using binding sets",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_context_init (IdeShortcutContext *self)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  priv->use_binding_sets = TRUE;
+}
+
+IdeShortcutContext *
+ide_shortcut_context_new (const gchar *name)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_CONTEXT,
+                       "name", name,
+                       NULL);
+}
+
+const gchar *
+ide_shortcut_context_get_name (IdeShortcutContext *self)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), NULL);
+
+  return priv->name;
+}
+
+IdeShortcutMatch
+ide_shortcut_context_activate (IdeShortcutContext     *self,
+                               GtkWidget              *widget,
+                               const IdeShortcutChord *chord)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+  IdeShortcutMatch match = IDE_SHORTCUT_MATCH_NONE;
+  Shortcut *shortcut = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), IDE_SHORTCUT_MATCH_NONE);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), IDE_SHORTCUT_MATCH_NONE);
+  g_return_val_if_fail (chord != NULL, IDE_SHORTCUT_MATCH_NONE);
+
+#if 0
+  g_print ("Looking up %s in table %p (of size %u)\n",
+           ide_shortcut_chord_to_string (chord),
+           priv->table,
+           ide_shortcut_chord_table_size (priv->table));
+
+  ide_shortcut_chord_table_printf (priv->table);
+#endif
+
+  if (priv->table != NULL)
+    match = ide_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&shortcut);
+
+  if (match == IDE_SHORTCUT_MATCH_EQUAL)
+    {
+      /*
+       * If we got a full match, but it failed to activate, we could potentially
+       * have another partial match. However, that lands squarely in the land of
+       * undefined behavior. So instead we just assume there was no match.
+       */
+      if (!shortcut_activate (shortcut, widget))
+        return IDE_SHORTCUT_MATCH_NONE;
+    }
+
+  return match;
+}
+
+static void
+ide_shortcut_context_add (IdeShortcutContext     *self,
+                          const IdeShortcutChord *chord,
+                          Shortcut               *shortcut)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+  IdeShortcutMatch match;
+  Shortcut *head = NULL;
+
+  g_assert (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_assert (shortcut != NULL);
+
+  if (priv->table == NULL)
+    {
+      priv->table = ide_shortcut_chord_table_new ();
+      ide_shortcut_chord_table_set_free_func (priv->table, shortcut_free);
+    }
+
+  /*
+   * If we find that there is another entry for this shortcut, we chain onto
+   * the end of that item. This allows us to call multiple signals, or
+   * interleave signals and actions.
+   */
+
+  match = ide_shortcut_chord_table_lookup (priv->table, chord, (gpointer *)&head);
+
+  if (match == IDE_SHORTCUT_MATCH_EQUAL)
+    {
+      while (head->next != NULL)
+        head = head->next;
+      head->next = shortcut;
+    }
+  else
+    {
+      ide_shortcut_chord_table_add (priv->table, chord, shortcut);
+    }
+}
+
+void
+ide_shortcut_context_add_action (IdeShortcutContext *self,
+                                 const gchar        *accel,
+                                 const gchar        *detailed_action_name)
+{
+  Shortcut *shortcut;
+  g_autofree gchar *action_name = NULL;
+  g_autofree gchar *prefix = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) action_target = NULL;
+  g_autoptr(IdeShortcutChord) chord = NULL;
+  const gchar *dot;
+  const gchar *name;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_return_if_fail (accel != NULL);
+  g_return_if_fail (detailed_action_name != NULL);
+
+  chord = ide_shortcut_chord_new_from_string (accel);
+
+  if (chord == NULL)
+    {
+      g_warning ("Failed to parse accelerator “%s”", accel);
+      return;
+    }
+
+  if (!g_action_parse_detailed_name (detailed_action_name, &action_name, &action_target, &error))
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  if (NULL != (dot = strchr (action_name, '.')))
+    {
+      name = &dot[1];
+      prefix = g_strndup (action_name, dot - action_name);
+    }
+  else
+    {
+      name = action_name;
+      prefix = NULL;
+    }
+
+  shortcut = g_slice_new0 (Shortcut);
+  shortcut->type = SHORTCUT_ACTION;
+  shortcut->action.prefix = prefix ? g_intern_string (prefix) : NULL;
+  shortcut->action.name = g_intern_string (name);
+  shortcut->action.param = g_steal_pointer (&action_target);
+
+  ide_shortcut_context_add (self, chord, shortcut);
+}
+
+void
+ide_shortcut_context_add_signal_va_list (IdeShortcutContext *self,
+                                         const gchar        *accel,
+                                         const gchar        *signal_name,
+                                         guint               n_args,
+                                         va_list             args)
+{
+  g_autoptr(GArray) params = NULL;
+  g_autoptr(IdeShortcutChord) chord = NULL;
+  g_autofree gchar *truncated_name = NULL;
+  const gchar *detail_str;
+  Shortcut *shortcut;
+  GQuark detail = 0;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_return_if_fail (accel != NULL);
+  g_return_if_fail (signal_name != NULL);
+
+  chord = ide_shortcut_chord_new_from_string (accel);
+
+  if (chord == NULL)
+    {
+      g_warning ("Failed to parse accelerator \"%s\"", accel);
+      return;
+    }
+
+  if (NULL != (detail_str = strstr (signal_name, "::")))
+    {
+      truncated_name = g_strndup (signal_name, detail_str - signal_name);
+      signal_name = truncated_name;
+      detail_str = &detail_str[2];
+      detail = g_quark_try_string (detail_str);
+    }
+
+  params = g_array_new (FALSE, FALSE, sizeof (GValue));
+  g_array_set_clear_func (params, (GDestroyNotify)g_value_unset);
+
+  for (; n_args > 0; n_args--)
+    {
+      g_autofree gchar *errstr = NULL;
+      GValue value = { 0 };
+      GType type;
+
+      type = va_arg (args, GType);
+
+      G_VALUE_COLLECT_INIT (&value, type, args, 0, &errstr);
+
+      if (errstr != NULL)
+        {
+          g_warning ("%s", errstr);
+          break;
+        }
+
+      g_array_append_val (params, value);
+    }
+
+  shortcut = g_slice_new0 (Shortcut);
+  shortcut->type = SHORTCUT_SIGNAL;
+  shortcut->signal.name = g_intern_string (signal_name);
+  shortcut->signal.detail = detail;
+  shortcut->signal.params = g_steal_pointer (&params);
+
+  ide_shortcut_context_add (self, chord, shortcut);
+}
+
+void
+ide_shortcut_context_add_signal (IdeShortcutContext *self,
+                                 const gchar        *accel,
+                                 const gchar        *signal_name,
+                                 guint               n_args,
+                                 ...)
+{
+  va_list args;
+
+  va_start (args, n_args);
+  ide_shortcut_context_add_signal_va_list (self, accel, signal_name, n_args, args);
+  va_end (args);
+}
+
+/**
+ * ide_shortcut_context_add_signalv:
+ * @self: a #IdeShortcutContext
+ * @accel: the accelerator for the shortcut
+ * @signal_name: the name of the signal
+ * @values: (element-type GObject.Value) (nullable) (transfer container): The
+ *   values to use when calling the signal.
+ *
+ * This is similar to ide_shortcut_context_add_signal() but is easier to use
+ * from language bindings.
+ *
+ * Note that this transfers ownership of the @values array.
+ */
+void
+ide_shortcut_context_add_signalv (IdeShortcutContext *self,
+                                  const gchar        *accel,
+                                  const gchar        *signal_name,
+                                  GArray             *values)
+{
+  g_autofree gchar *truncated_name = NULL;
+  g_autoptr(IdeShortcutChord) chord = NULL;
+  const gchar *detail_str;
+  Shortcut *shortcut;
+  GQuark detail = 0;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (self));
+  g_return_if_fail (accel != NULL);
+  g_return_if_fail (signal_name != NULL);
+
+  chord = ide_shortcut_chord_new_from_string (accel);
+
+  if (chord == NULL)
+    {
+      g_warning ("Failed to parse accelerator \"%s\"", accel);
+      return;
+    }
+
+  if (values == NULL)
+    {
+      values = g_array_new (FALSE, FALSE, sizeof (GValue));
+      g_array_set_clear_func (values, (GDestroyNotify)g_value_unset);
+    }
+
+  if (NULL != (detail_str = strstr (signal_name, "::")))
+    {
+      truncated_name = g_strndup (signal_name, detail_str - signal_name);
+      signal_name = truncated_name;
+      detail_str = &detail_str[2];
+      detail = g_quark_try_string (detail_str);
+    }
+
+  shortcut = g_slice_new0 (Shortcut);
+  shortcut->type = SHORTCUT_SIGNAL;
+  shortcut->signal.name = g_intern_string (signal_name);
+  shortcut->signal.detail = detail;
+  shortcut->signal.params = values;
+
+  ide_shortcut_context_add (self, chord, shortcut);
+}
+
+gboolean
+ide_shortcut_context_remove (IdeShortcutContext *self,
+                             const gchar        *accel)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+  g_autoptr(IdeShortcutChord) chord = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+  g_return_val_if_fail (accel != NULL, FALSE);
+
+  chord = ide_shortcut_chord_new_from_string (accel);
+
+  if (chord != NULL && priv->table != NULL)
+    return ide_shortcut_chord_table_remove (priv->table, chord);
+
+  return FALSE;
+}
+
+gboolean
+ide_shortcut_context_load_from_data (IdeShortcutContext  *self,
+                                     const gchar         *data,
+                                     gssize               len,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+  g_return_val_if_fail (data != NULL, FALSE);
+
+  if (len < 0)
+    len = strlen (data);
+
+  g_set_error (error,
+               G_IO_ERROR,
+               G_IO_ERROR_INVALID_DATA,
+               "Failed to parse shortcut data");
+
+  return FALSE;
+}
+
+gboolean
+ide_shortcut_context_load_from_resource (IdeShortcutContext  *self,
+                                         const gchar         *resource_path,
+                                         GError             **error)
+{
+  g_autoptr(GBytes) bytes = NULL;
+  const gchar *endptr = NULL;
+  const gchar *data;
+  gsize len;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), FALSE);
+
+  if (NULL == (bytes = g_resources_lookup_data (resource_path, 0, error)))
+    return FALSE;
+
+  data = g_bytes_get_data (bytes, &len);
+
+  if (!g_utf8_validate (data, len, &endptr))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Invalid UTF-8 at offset %u",
+                   (guint)(endptr - data));
+      return FALSE;
+    }
+
+  return ide_shortcut_context_load_from_data (self, data, len, error);
+}
+
+IdeShortcutChordTable *
+_ide_shortcut_context_get_table (IdeShortcutContext *self)
+{
+  IdeShortcutContextPrivate *priv = ide_shortcut_context_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTEXT (self), NULL);
+
+  return priv->table;
+}
diff --git a/libide/shortcuts/ide-shortcut-context.h b/libide/shortcuts/ide-shortcut-context.h
new file mode 100644
index 0000000..e2bce96
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-context.h
@@ -0,0 +1,66 @@
+/* ide-shortcut-context.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 2 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 IDE_SHORTCUT_CONTEXT_H
+#define IDE_SHORTCUT_CONTEXT_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-chord.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTEXT (ide_shortcut_context_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutContext, ide_shortcut_context, IDE, SHORTCUT_CONTEXT, GObject)
+
+IdeShortcutContext *ide_shortcut_context_new                (const gchar             *name);
+const gchar        *ide_shortcut_context_get_name           (IdeShortcutContext      *self);
+IdeShortcutMatch    ide_shortcut_context_activate           (IdeShortcutContext      *self,
+                                                             GtkWidget               *widget,
+                                                             const IdeShortcutChord  *chord);
+void                ide_shortcut_context_add_action         (IdeShortcutContext      *self,
+                                                             const gchar             *accel,
+                                                             const gchar             *detailed_action_name);
+void                ide_shortcut_context_add_signal         (IdeShortcutContext      *self,
+                                                             const gchar             *accel,
+                                                             const gchar             *signal_name,
+                                                             guint                    n_args,
+                                                             ...);
+void                ide_shortcut_context_add_signal_va_list (IdeShortcutContext      *self,
+                                                             const gchar             *accel,
+                                                             const gchar             *signal_name,
+                                                             guint                    n_args,
+                                                             va_list                  args);
+void                ide_shortcut_context_add_signalv        (IdeShortcutContext      *self,
+                                                             const gchar             *accel,
+                                                             const gchar             *signal_name,
+                                                             GArray                  *values);
+gboolean            ide_shortcut_context_remove             (IdeShortcutContext      *self,
+                                                             const gchar             *accel);
+gboolean            ide_shortcut_context_load_from_data     (IdeShortcutContext      *self,
+                                                             const gchar             *data,
+                                                             gssize                   len,
+                                                             GError                 **error);
+gboolean            ide_shortcut_context_load_from_resource (IdeShortcutContext      *self,
+                                                             const gchar             *resource_path,
+                                                             GError                 **error);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTEXT_H */
diff --git a/libide/shortcuts/ide-shortcut-controller.c b/libide/shortcuts/ide-shortcut-controller.c
new file mode 100644
index 0000000..2c2cb0b
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.c
@@ -0,0 +1,781 @@
+/* ide-shortcut-controller.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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-controller"
+
+#include "ide-shortcut-context.h"
+#include "ide-shortcut-controller.h"
+#include "ide-shortcut-manager.h"
+
+typedef struct
+{
+  /*
+   * This is the widget for which we are the shortcut controller. There are
+   * zero or one shortcut controller for a given widget. These are persistent
+   * and dispatch events to the current IdeShortcutContext (which can be
+   * changed upon theme changes or shortcuts emitting the ::set-context signal.
+   */
+  GtkWidget *widget;
+
+  /*
+   * This is the current context for the controller. These are collections of
+   * shortcuts to signals, actions, etc. The context can be changed in reaction
+   * to different events.
+   */
+  IdeShortcutContext *context;
+
+  /*
+   * This is the IdeShortcutContext used for commands attached to the controller.
+   * Commands are operations which the user can override and will be activated
+   * after the current @context.
+   */
+  IdeShortcutContext *command_context;
+
+  /*
+   * If we are building a chord, it will be tracked here. Each incoming
+   * GdkEventKey will contribute to the creation of this chord.
+   */
+  IdeShortcutChord *current_chord;
+
+  /*
+   * This is an array of Command elements which are used to build the shortcuts
+   * window and list of commands that the user can override.
+   */
+  GArray *commands;
+
+  /*
+   * This is a pointer to the root controller for the window. We register with
+   * the root controller so that keybindings can be activated even when the
+   * focus widget is somewhere else.
+   */
+  IdeShortcutController *root;
+
+  /*
+   * The root controller keeps track of the children controllers in the window.
+   * Instead of allocating GList entries, we use an inline GList for the Queue
+   * link nodes.
+   */
+  GQueue descendants;
+  GList  descendants_link;
+
+  /*
+   * Signal handlers to react to various changes in the system.
+   */
+  gulong hierarchy_changed_handler;
+  gulong widget_destroy_handler;
+} IdeShortcutControllerPrivate;
+
+typedef struct
+{
+  const gchar *id;
+  const gchar *group;
+  const gchar *title;
+  const gchar *subtitle;
+} Command;
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_CURRENT_CHORD,
+  PROP_WIDGET,
+  N_PROPS
+};
+
+enum {
+  RESET,
+  SET_CONTEXT_NAMED,
+  N_SIGNALS
+};
+
+struct _IdeShortcutController { GObject object; };
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutController, ide_shortcut_controller, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint       signals [N_SIGNALS];
+static GQuark      root_quark;
+static GQuark      controller_quark;
+
+static void ide_shortcut_controller_connect    (IdeShortcutController *self);
+static void ide_shortcut_controller_disconnect (IdeShortcutController *self);
+
+static gboolean
+ide_shortcut_controller_is_mapped (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  return priv->widget != NULL && gtk_widget_get_mapped (priv->widget);
+}
+
+static void
+ide_shortcut_controller_add (IdeShortcutController *self,
+                             IdeShortcutController *descendant)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+  g_object_ref (descendant);
+
+  if (ide_shortcut_controller_is_mapped (descendant))
+    g_queue_push_head_link (&priv->descendants, &dpriv->descendants_link);
+  else
+    g_queue_push_tail_link (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_remove (IdeShortcutController *self,
+                                IdeShortcutController *descendant)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutControllerPrivate *dpriv = ide_shortcut_controller_get_instance_private (descendant);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (descendant));
+
+  g_queue_unlink (&priv->descendants, &dpriv->descendants_link);
+}
+
+static void
+ide_shortcut_controller_widget_destroy (IdeShortcutController *self,
+                                        GtkWidget             *widget)
+{
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  ide_shortcut_controller_disconnect (self);
+}
+
+static void
+ide_shortcut_controller_widget_hierarchy_changed (IdeShortcutController *self,
+                                                  GtkWidget             *previous_toplevel,
+                                                  GtkWidget             *widget)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  g_object_ref (self);
+
+  /*
+   * Here we register our controller with the toplevel controller. If that
+   * widget doesn't yet have a placeholder toplevel controller, then we
+   * create that and attach to it.
+   *
+   * The toplevel controller is used to dispatch events from the window
+   * to any controller that could be activating for the window.
+   */
+
+  if (priv->root != NULL)
+    {
+      ide_shortcut_controller_remove (priv->root, self);
+      g_clear_object (&priv->root);
+    }
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (toplevel != widget)
+    {
+      priv->root = g_object_get_qdata (G_OBJECT (toplevel), root_quark);
+      if (priv->root == NULL)
+        priv->root = ide_shortcut_controller_new (toplevel);
+      ide_shortcut_controller_add (priv->root, self);
+    }
+
+  g_object_unref (self);
+}
+
+static void
+ide_shortcut_controller_disconnect (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (priv->widget));
+
+  g_signal_handler_disconnect (priv->widget, priv->widget_destroy_handler);
+  priv->widget_destroy_handler = 0;
+
+  g_signal_handler_disconnect (priv->widget, priv->hierarchy_changed_handler);
+  priv->hierarchy_changed_handler = 0;
+}
+
+static void
+ide_shortcut_controller_connect (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (priv->widget));
+
+  priv->widget_destroy_handler =
+    g_signal_connect_swapped (priv->widget,
+                              "destroy",
+                              G_CALLBACK (ide_shortcut_controller_widget_destroy),
+                              self);
+
+  priv->hierarchy_changed_handler =
+    g_signal_connect_swapped (priv->widget,
+                              "hierarchy-changed",
+                              G_CALLBACK (ide_shortcut_controller_widget_hierarchy_changed),
+                              self);
+
+  ide_shortcut_controller_widget_hierarchy_changed (self, NULL, priv->widget);
+}
+
+static void
+ide_shortcut_controller_set_widget (IdeShortcutController *self,
+                                    GtkWidget             *widget)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (widget != priv->widget)
+    {
+      if (priv->widget != NULL)
+        {
+          ide_shortcut_controller_disconnect (self);
+          g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+          priv->widget = NULL;
+        }
+
+      if (widget != NULL && widget != priv->widget)
+        {
+          priv->widget = widget;
+          g_object_add_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+          ide_shortcut_controller_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WIDGET]);
+    }
+}
+
+static void
+ide_shortcut_controller_emit_reset (IdeShortcutController *self)
+{
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+
+  g_signal_emit (self, signals[RESET], 0);
+}
+
+void
+ide_shortcut_controller_set_context (IdeShortcutController *self,
+                                     IdeShortcutContext    *context)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (!context || IDE_IS_SHORTCUT_CONTEXT (context));
+
+  if (g_set_object (&priv->context, context))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CONTEXT]);
+      ide_shortcut_controller_emit_reset (self);
+    }
+}
+
+static void
+ide_shortcut_controller_real_set_context_named (IdeShortcutController *self,
+                                                const gchar           *name)
+{
+  g_autoptr(IdeShortcutContext) context = NULL;
+  IdeShortcutManager *manager;
+  IdeShortcutTheme *theme;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (name != NULL);
+
+  manager = ide_shortcut_manager_get_default ();
+  theme = ide_shortcut_manager_get_theme (manager);
+  context = ide_shortcut_theme_find_context_by_name (theme, name);
+
+  ide_shortcut_controller_set_context (self, context);
+}
+
+static void
+ide_shortcut_controller_finalize (GObject *object)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  if (priv->widget != NULL)
+    {
+      g_object_remove_weak_pointer (G_OBJECT (priv->widget), (gpointer *)&priv->widget);
+      priv->widget = NULL;
+    }
+
+  g_clear_pointer (&priv->commands, g_array_unref);
+
+  g_clear_object (&priv->command_context);
+  g_clear_object (&priv->context);
+  g_clear_object (&priv->root);
+
+  while (priv->descendants.length > 0)
+    g_queue_unlink (&priv->descendants, priv->descendants.head);
+
+  G_OBJECT_CLASS (ide_shortcut_controller_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_controller_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, priv->context);
+      break;
+
+    case PROP_CURRENT_CHORD:
+      g_value_set_boxed (value, ide_shortcut_controller_get_current_chord (self));
+      break;
+
+    case PROP_WIDGET:
+      g_value_set_object (value, priv->widget);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_controller_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeShortcutController *self = (IdeShortcutController *)object;
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_shortcut_controller_set_context (self, g_value_get_object (value));
+      break;
+
+    case PROP_WIDGET:
+      ide_shortcut_controller_set_widget (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_controller_class_init (IdeShortcutControllerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_controller_finalize;
+  object_class->get_property = ide_shortcut_controller_get_property;
+  object_class->set_property = ide_shortcut_controller_set_property;
+
+  properties [PROP_CURRENT_CHORD] =
+    g_param_spec_boxed ("current-chord",
+                        "Current Chord",
+                        "The current chord for the controller",
+                        IDE_TYPE_SHORTCUT_CHORD,
+                        (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The current context of the controller",
+                         IDE_TYPE_SHORTCUT_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_WIDGET] =
+    g_param_spec_object ("widget",
+                         "Widget",
+                         "The widget for which the controller attached",
+                         GTK_TYPE_WIDGET,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeShortcutController::reset:
+   *
+   * This signal is emitted when the shortcut controller is requesting
+   * the widget to reset any state it may have regarding the shortcut
+   * controller. Such an example might be a modal system that lives
+   * outside the controller whose state should be cleared in response
+   * to the controller changing modes.
+   */
+  signals [RESET] =
+    g_signal_new_class_handler ("reset",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                NULL, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * IdeShortcutController::set-context-named:
+   * @self: An #IdeShortcutController
+   * @name: The name of the context
+   *
+   * This changes the current context on the #IdeShortcutController to be the
+   * context matching @name. This is found by looking up the context by name
+   * in the active #IdeShortcutTheme.
+   */
+  signals [SET_CONTEXT_NAMED] =
+    g_signal_new_class_handler ("set-context-named",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_shortcut_controller_real_set_context_named),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_STRING);
+
+  controller_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER");
+  root_quark = g_quark_from_static_string ("IDE_SHORTCUT_CONTROLLER_ROOT");
+}
+
+static void
+ide_shortcut_controller_init (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_queue_init (&priv->descendants);
+
+  priv->descendants_link.data = self;
+}
+
+IdeShortcutController *
+ide_shortcut_controller_new (GtkWidget *widget)
+{
+  IdeShortcutController *ret;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  if (NULL != (ret = g_object_get_qdata (G_OBJECT (widget), controller_quark)))
+    return g_object_ref (ret);
+
+  ret = g_object_new (IDE_TYPE_SHORTCUT_CONTROLLER,
+                      "widget", widget,
+                      NULL);
+
+  g_object_set_qdata_full (G_OBJECT (widget),
+                           controller_quark,
+                           g_object_ref (ret),
+                           g_object_unref);
+
+  return ret;
+}
+
+/**
+ * ide_shortcut_controller_find:
+ *
+ * Finds the registered #IdeShortcutController for a widget.
+ *
+ * Returns: (not nullable) (transfer none): An #IdeShortcutController or %NULL.
+ */
+IdeShortcutController *
+ide_shortcut_controller_find (GtkWidget *widget)
+{
+  IdeShortcutController *controller;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
+
+  if (controller == NULL)
+    {
+      /* We want to pass a borrowed reference */
+      g_object_unref (ide_shortcut_controller_new (widget));
+      controller = g_object_get_qdata (G_OBJECT (widget), controller_quark);
+    }
+
+  return controller;
+}
+
+/**
+ * ide_shortcut_controller_get_context:
+ * @self: An #IdeShortcutController
+ *
+ * This function gets the #IdeShortcutController:context property, which
+ * is the current context to dispatch events to. An #IdeShortcutContext
+ * is a group of keybindings that may be activated in response to a
+ * single or series of #GdkEventKey.
+ *
+ * Returns: (transfer none) (nullable): An #IdeShortcutContext or %NULL.
+ */
+IdeShortcutContext *
+ide_shortcut_controller_get_context (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), NULL);
+
+  if (priv->widget == NULL)
+    return NULL;
+
+  if (priv->context == NULL)
+    {
+      IdeShortcutManager *manager;
+      IdeShortcutTheme *theme;
+
+      manager = ide_shortcut_manager_get_default ();
+      theme = ide_shortcut_manager_get_theme (manager);
+
+      /*
+       * If we have not set an explicit context, then we want to just return
+       * our borrowed context so if the theme changes we adapt.
+       */
+
+      return ide_shortcut_theme_find_default_context (theme, priv->widget);
+    }
+
+  return priv->context;
+}
+
+static IdeShortcutMatch
+ide_shortcut_controller_process (IdeShortcutController  *self,
+                                 const IdeShortcutChord *chord)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutContext *context;
+  IdeShortcutMatch match = IDE_SHORTCUT_MATCH_NONE;
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_assert (chord != NULL);
+
+  /* Short-circuit if we can't make forward progress */
+  if (priv->widget == NULL ||
+      !gtk_widget_get_visible (priv->widget) ||
+      !gtk_widget_is_sensitive (priv->widget))
+    return IDE_SHORTCUT_MATCH_NONE;
+
+  /* Try to activate our current context */
+  context = ide_shortcut_controller_get_context (self);
+  if (match == IDE_SHORTCUT_MATCH_NONE && context != NULL)
+    match = ide_shortcut_context_activate (context, priv->widget, chord);
+
+  /* If we didn't find a match, try our command context */
+  context = priv->command_context;
+  if (match == IDE_SHORTCUT_MATCH_NONE && context != NULL)
+    match = ide_shortcut_context_activate (context, priv->widget, chord);
+
+  /* Try to activate one of our descendant controllers */
+  for (GList *iter = priv->descendants.head;
+       match == IDE_SHORTCUT_MATCH_NONE && iter != NULL;
+       iter = iter->next)
+    {
+      IdeShortcutController *descendant = iter->data;
+      match = ide_shortcut_controller_process (descendant, chord);
+    }
+
+  return match;
+}
+
+/**
+ * ide_shortcut_controller_handle_event:
+ * @self: An #IdeShortcutController
+ * @event: A #GdkEventKey
+ *
+ * This function uses @event to determine if the current context has a shortcut
+ * registered matching the event. If so, the shortcut will be dispatched and
+ * %TRUE is returned.
+ *
+ * Otherwise, %FALSE is returned.
+ *
+ * Returns: %TRUE if @event has been handled, otherwise %FALSE.
+ */
+gboolean
+ide_shortcut_controller_handle_event (IdeShortcutController *self,
+                                      const GdkEventKey     *event)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutMatch match;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  /*
+   * This handles the activation of the event starting from this context,
+   * and working our way down into the children controllers.
+   *
+   * We process things in the order of:
+   *
+   *   1) Our current shortcut context
+   *   2) Our current shortcut context for "commands"
+   *   3) Each of our registered controllers.
+   *
+   * This gets a bit complicated once we start talking about chords. A chord is
+   * a sequence of GdkEventKey which can activate a shortcut. That might be
+   * something like Ctrl+X|Ctrl+O. Ctrl+X does not activate something on its
+   * own, but if the following event is a CTRL+O, it will activate that
+   * shortcut.
+   *
+   * This means we need to stash the chord sequence while we have partial
+   * matches up until we get a match. If no match is found (nor a partial),
+   * then we can ignore the event and return GDK_EVENT_PROPAGATE.
+   *
+   * If we swallow the event, because we are building a chord, then we will
+   * return GDK_EVENT_STOP and stash the chord for future use.
+   *
+   * While unfortunate, we do not try to handle a situation where we have a
+   * collision between an exact match and a partial match. The first item we
+   * come across wins. This is considered undefined behavior.
+   */
+
+  if (priv->current_chord == NULL)
+    {
+      priv->current_chord = ide_shortcut_chord_new_from_event (event);
+      if (priv->current_chord == NULL)
+        return GDK_EVENT_PROPAGATE;
+    }
+  else
+    {
+      if (!ide_shortcut_chord_append_event (priv->current_chord, event))
+        {
+          g_clear_pointer (&priv->current_chord, ide_shortcut_chord_free);
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]);
+          return GDK_EVENT_PROPAGATE;
+        }
+    }
+
+  g_assert (priv->current_chord != NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]);
+
+  {
+    g_autofree gchar *str = ide_shortcut_chord_to_string (priv->current_chord);
+    g_debug ("Chord = %s", str);
+  }
+
+  match = ide_shortcut_controller_process (self, priv->current_chord);
+
+  if (match != IDE_SHORTCUT_MATCH_PARTIAL)
+    {
+      g_clear_pointer (&priv->current_chord, ide_shortcut_chord_free);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_CHORD]);
+    }
+
+  g_debug ("match = %d", match);
+
+  return (match ? GDK_EVENT_STOP : GDK_EVENT_PROPAGATE);
+}
+
+static IdeShortcutContext *
+ide_shortcut_controller_get_command_context (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_CONTROLLER (self));
+
+  if (priv->command_context == NULL)
+    priv->command_context = g_object_new (IDE_TYPE_SHORTCUT_CONTEXT,
+                                          "use-binding-sets", FALSE,
+                                          NULL);
+
+  return priv->command_context;
+}
+
+/**
+ * ide_shortcut_controller_add_command_signal: (skip)
+ * @self: An #IdeShortcutController
+ * @command_id: the command-id such as "org.gnome.builder.plugins.foo.bar"
+ * @default_accel: the accelerator for the default key theme
+ * @group: the group to place the shortcut within the shortcuts overview
+ * @title: the title for the shortcut
+ * @subtitle: (nullable): an optional subtitle for the command
+ * @signal_name: the name of the signal to activate on the controllers widget
+ * @n_args: the number of argument pairs
+ *
+ * This adds a command to the controller which will activate the signal @signal_name
+ * on the attached #GtkWidget. Use @n_args followed by pairs of (#GType, value) to
+ * specify the arguments for the signal. This is similar to
+ * gtk_binding_entry_add_signal().
+ *
+ * By registering a command on a controller directly, the shortcuts overview can
+ * display the shortcut in the shortcuts window as well as allow the user to
+ * override the accelerator. Where as the user cannot override operations found
+ * directly in #IdeShortcutContext's as provided by themes.
+ */
+void
+ide_shortcut_controller_add_command_signal (IdeShortcutController *self,
+                                            const gchar           *command_id,
+                                            const gchar           *default_accel,
+                                            const gchar           *group,
+                                            const gchar           *title,
+                                            const gchar           *subtitle,
+                                            const gchar           *signal_name,
+                                            guint                  n_args,
+                                            ...)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+  IdeShortcutContext *command_context;
+  Command command = { 0 };
+  va_list args;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self));
+  g_return_if_fail (command_id != NULL);
+  g_return_if_fail (group != NULL);
+  g_return_if_fail (title != NULL);
+  g_return_if_fail (signal_name != NULL);
+
+  command.id = g_intern_string (command_id);
+  command.group = g_intern_string (group);
+  command.title = g_intern_string (title);
+  command.subtitle = g_intern_string (subtitle);
+
+  if (priv->commands == NULL)
+    priv->commands = g_array_new (FALSE, FALSE, sizeof (Command));
+
+  g_array_append_val (priv->commands, command);
+
+  command_context = ide_shortcut_controller_get_command_context (self);
+
+  va_start (args, n_args);
+  ide_shortcut_context_add_signal_va_list (command_context,
+                                           default_accel,
+                                           signal_name,
+                                           n_args,
+                                           args);
+  va_end (args);
+}
+
+/**
+ * ide_shortcut_controller_get_current_chord:
+ * @self: a #IdeShortcutController
+ *
+ * This method gets the #IdeShortcutController:current-chord property.
+ *
+ * This is useful if you want to monitor in-progress chord building.
+ *
+ * Returns: (transfer none) (nullable): A #IdeShortcutChord or %NULL.
+ */
+const IdeShortcutChord *
+ide_shortcut_controller_get_current_chord (IdeShortcutController *self)
+{
+  IdeShortcutControllerPrivate *priv = ide_shortcut_controller_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_CONTROLLER (self), NULL);
+
+  return priv->current_chord;
+}
diff --git a/libide/shortcuts/ide-shortcut-controller.h b/libide/shortcuts/ide-shortcut-controller.h
new file mode 100644
index 0000000..17c4e62
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-controller.h
@@ -0,0 +1,52 @@
+/* ide-shortcut-controller.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 2 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 IDE_SHORTCUT_CONTROLLER_H
+#define IDE_SHORTCUT_CONTROLLER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_CONTROLLER (ide_shortcut_controller_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutController, ide_shortcut_controller, IDE, SHORTCUT_CONTROLLER, GObject)
+
+IdeShortcutController  *ide_shortcut_controller_new                (GtkWidget             *widget);
+gboolean                ide_shortcut_controller_handle_event       (IdeShortcutController *self,
+                                                                    const GdkEventKey     *event);
+IdeShortcutController  *ide_shortcut_controller_find               (GtkWidget             *widget);
+IdeShortcutContext     *ide_shortcut_controller_get_context        (IdeShortcutController *self);
+void                    ide_shortcut_controller_set_context        (IdeShortcutController *self,
+                                                                    IdeShortcutContext    *context);
+const IdeShortcutChord *ide_shortcut_controller_get_current_chord  (IdeShortcutController *self);
+void                    ide_shortcut_controller_add_command_signal (IdeShortcutController *self,
+                                                                    const gchar           *command_id,
+                                                                    const gchar           *default_accel,
+                                                                    const gchar           *group,
+                                                                    const gchar           *title,
+                                                                    const gchar           *subtitle,
+                                                                    const gchar           *signal_name,
+                                                                    guint                  n_args,
+                                                                    ...);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_CONTROLLER_H */
diff --git a/libide/shortcuts/ide-shortcut-label.c b/libide/shortcuts/ide-shortcut-label.c
new file mode 100644
index 0000000..f383026
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-label.c
@@ -0,0 +1,212 @@
+/* ide-shortcut-label.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-label"
+
+#include "ide-shortcut-label.h"
+
+struct _IdeShortcutLabel
+{
+  GtkBox            parent_instance;
+  IdeShortcutChord *chord;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCELERATOR,
+  PROP_CHORD,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeShortcutLabel, ide_shortcut_label, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_label_finalize (GObject *object)
+{
+  IdeShortcutLabel *self = (IdeShortcutLabel *)object;
+
+  g_clear_pointer (&self->chord, ide_shortcut_chord_free);
+
+  G_OBJECT_CLASS (ide_shortcut_label_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_label_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      g_value_take_string (value, ide_shortcut_label_get_accelerator (self));
+      break;
+
+    case PROP_CHORD:
+      g_value_set_boxed (value, ide_shortcut_label_get_chord (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      ide_shortcut_label_set_accelerator (self, g_value_get_string (value));
+      break;
+
+    case PROP_CHORD:
+      ide_shortcut_label_set_chord (self, g_value_get_boxed (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_class_init (IdeShortcutLabelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_label_finalize;
+  object_class->get_property = ide_shortcut_label_get_property;
+  object_class->set_property = ide_shortcut_label_set_property;
+
+  properties [PROP_ACCELERATOR] =
+    g_param_spec_string ("accelerator",
+                         "Accelerator",
+                         "The accelerator for the label",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CHORD] =
+    g_param_spec_boxed ("chord",
+                         "Chord",
+                         "The chord for the label",
+                         IDE_TYPE_SHORTCUT_CHORD,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_label_init (IdeShortcutLabel *self)
+{
+  gtk_box_set_spacing (GTK_BOX (self), 12);
+}
+
+GtkWidget *
+ide_shortcut_label_new (void)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_LABEL, NULL);
+}
+
+gchar *
+ide_shortcut_label_get_accelerator (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  if (self->chord == NULL)
+    return NULL;
+
+  return ide_shortcut_chord_to_string (self->chord);
+}
+
+void
+ide_shortcut_label_set_accelerator (IdeShortcutLabel *self,
+                                    const gchar      *accelerator)
+{
+  g_autoptr(IdeShortcutChord) chord = NULL;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  if (accelerator != NULL)
+    chord = ide_shortcut_chord_new_from_string (accelerator);
+
+  ide_shortcut_label_set_chord (self, chord);
+}
+
+void
+ide_shortcut_label_set_chord (IdeShortcutLabel       *self,
+                              const IdeShortcutChord *chord)
+{
+  if (!ide_shortcut_chord_equal (chord, self->chord))
+    {
+      g_autofree gchar *accel = NULL;
+
+      ide_shortcut_chord_free (self->chord);
+      self->chord = ide_shortcut_chord_copy (chord);
+
+      if (self->chord != NULL)
+        accel = ide_shortcut_chord_to_string (self->chord);
+
+      gtk_container_foreach (GTK_CONTAINER (self),
+                             (GtkCallback) gtk_widget_destroy,
+                             NULL);
+
+      if (accel != NULL)
+        {
+          g_auto(GStrv) parts = NULL;
+
+          parts = g_strsplit (accel, "|", 0);
+
+          for (guint i = 0; parts[i]; i++)
+            {
+              GtkWidget *label;
+
+              label = g_object_new (GTK_TYPE_SHORTCUT_LABEL,
+                                    "accelerator", parts[i],
+                                    "visible", TRUE,
+                                    NULL);
+              gtk_container_add (GTK_CONTAINER (self), label);
+            }
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCELERATOR]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHORD]);
+    }
+}
+
+/**
+ * ide_shortcut_label_get_chord:
+ * @self: a #IdeShortcutLabel
+ *
+ * Gets the chord for the label, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): A #IdeShortcutChord or %NULL
+ */
+const IdeShortcutChord *
+ide_shortcut_label_get_chord (IdeShortcutLabel *self)
+{
+  return self->chord;
+}
diff --git a/libide/shortcuts/ide-shortcut-label.h b/libide/shortcuts/ide-shortcut-label.h
new file mode 100644
index 0000000..7f4688d
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-label.h
@@ -0,0 +1,42 @@
+/* ide-shortcut-label.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_LABEL_H
+#define IDE_SHORTCUT_LABEL_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-chord.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_LABEL (ide_shortcut_label_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutLabel, ide_shortcut_label, IDE, SHORTCUT_LABEL, GtkBox)
+
+GtkWidget              *ide_shortcut_label_new             (void);
+gchar                  *ide_shortcut_label_get_accelerator (IdeShortcutLabel       *self);
+void                    ide_shortcut_label_set_accelerator (IdeShortcutLabel       *self,
+                                                            const gchar            *accelerator);
+void                    ide_shortcut_label_set_chord       (IdeShortcutLabel       *self,
+                                                            const IdeShortcutChord *chord);
+const IdeShortcutChord *ide_shortcut_label_get_chord       (IdeShortcutLabel       *self);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_LABEL_H */
diff --git a/libide/shortcuts/ide-shortcut-manager.c b/libide/shortcuts/ide-shortcut-manager.c
new file mode 100644
index 0000000..fbeaca9
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.c
@@ -0,0 +1,958 @@
+/* ide-shortcut-manager.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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-manager.h"
+
+#include "ide-shortcut-private.h"
+
+#include "ide-shortcut-controller.h"
+#include "ide-shortcut-label.h"
+#include "ide-shortcut-manager.h"
+#include "ide-shortcuts-group.h"
+#include "ide-shortcuts-section.h"
+#include "ide-shortcuts-shortcut.h"
+
+typedef struct
+{
+  IdeShortcutTheme *theme;
+  GPtrArray        *themes;
+  gchar            *user_dir;
+  GNode            *root;
+  GQueue            search_path;
+} IdeShortcutManagerPrivate;
+
+enum {
+  PROP_0,
+  PROP_THEME,
+  PROP_THEME_NAME,
+  PROP_USER_DIR,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+static void initable_iface_init   (GInitableIface      *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeShortcutManager, ide_shortcut_manager, G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeShortcutManager)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static gboolean
+free_node_data (GNode    *node,
+                gpointer  user_data)
+{
+  IdeShortcutNodeData *data = node->data;
+
+  g_slice_free (IdeShortcutNodeData, data);
+
+  return FALSE;
+}
+
+static void
+ide_shortcut_manager_finalize (GObject *object)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  if (priv->root != NULL)
+    {
+      g_node_traverse (priv->root, G_IN_ORDER, G_TRAVERSE_ALL, -1, free_node_data, NULL);
+      g_node_destroy (priv->root);
+      priv->root = NULL;
+    }
+
+  g_clear_pointer (&priv->themes, g_ptr_array_unref);
+  g_clear_pointer (&priv->user_dir, g_free);
+  g_clear_object (&priv->theme);
+
+  G_OBJECT_CLASS (ide_shortcut_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_manager_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      g_value_set_object (value, ide_shortcut_manager_get_theme (self));
+      break;
+
+    case PROP_THEME_NAME:
+      g_value_set_string (value, ide_shortcut_manager_get_theme_name (self));
+      break;
+
+    case PROP_USER_DIR:
+      g_value_set_string (value, ide_shortcut_manager_get_user_dir (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_manager_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      ide_shortcut_manager_set_theme (self, g_value_get_object (value));
+      break;
+
+    case PROP_THEME_NAME:
+      ide_shortcut_manager_set_theme_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_USER_DIR:
+      ide_shortcut_manager_set_user_dir (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_manager_class_init (IdeShortcutManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_manager_finalize;
+  object_class->get_property = ide_shortcut_manager_get_property;
+  object_class->set_property = ide_shortcut_manager_set_property;
+
+  properties [PROP_THEME] =
+    g_param_spec_object ("theme",
+                         "Theme",
+                         "The current key theme.",
+                         IDE_TYPE_SHORTCUT_THEME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_THEME_NAME] =
+    g_param_spec_string ("theme-name",
+                         "Theme Name",
+                         "The name of the current theme",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USER_DIR] =
+    g_param_spec_string ("user-dir",
+                         "User Dir",
+                         "The directory for saved user modifications",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_shortcut_manager_init (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  priv->themes = g_ptr_array_new_with_free_func (g_object_unref);
+  priv->root = g_node_new (NULL);
+}
+
+static void
+ide_shortcut_manager_load_directory (IdeShortcutManager  *self,
+                                     const gchar         *directory,
+                                     GCancellable        *cancellable)
+{
+  g_autoptr(GDir) dir = NULL;
+  const gchar *name;
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (directory != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!g_file_test (directory, G_FILE_TEST_IS_DIR))
+    return;
+
+  if (NULL == (dir = g_dir_open (directory, 0, NULL)))
+    return;
+
+  while (NULL != (name = g_dir_read_name (dir)))
+    {
+      g_autofree gchar *path = g_build_filename (directory, name, NULL);
+      g_autoptr(IdeShortcutTheme) theme = NULL;
+      g_autoptr(GError) local_error = NULL;
+
+      theme = ide_shortcut_theme_new (NULL);
+
+      if (ide_shortcut_theme_load_from_path (theme, path, cancellable, &local_error))
+        ide_shortcut_manager_add_theme (self, theme);
+      else
+        g_warning ("%s", local_error->message);
+    }
+}
+
+static void
+ide_shortcut_manager_load_resources (IdeShortcutManager *self,
+                                     const gchar        *resource_dir,
+                                     GCancellable       *cancellable)
+{
+  g_auto(GStrv) children = NULL;
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (resource_dir != NULL);
+  g_assert (g_str_has_prefix (resource_dir, "resource://"));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  children = g_resources_enumerate_children (resource_dir, 0, NULL);
+
+  if (children != NULL)
+    {
+      for (guint i = 0; children[i] != NULL; i++)
+        {
+          g_autofree gchar *path = g_build_filename (resource_dir, children[i], NULL);
+          g_autoptr(IdeShortcutTheme) theme = NULL;
+          g_autoptr(GError) local_error = NULL;
+          g_autoptr(GBytes) bytes = NULL;
+          const gchar *data;
+          gsize len = 0;
+
+          if (NULL == (bytes = g_resources_lookup_data (path, 0, NULL)))
+            continue;
+
+          data = g_bytes_get_data (bytes, &len);
+          theme = ide_shortcut_theme_new (NULL);
+
+          if (ide_shortcut_theme_load_from_data (theme, data, len, &local_error))
+            ide_shortcut_manager_add_theme (self, theme);
+          else
+            g_warning ("%s", local_error->message);
+        }
+    }
+}
+
+static gboolean
+ide_shortcut_manager_initiable_init (GInitable     *initable,
+                                     GCancellable  *cancellable,
+                                     GError       **error)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)initable;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (const GList *iter = priv->search_path.tail; iter != NULL; iter = iter->prev)
+    {
+      const gchar *directory = iter->data;
+
+      if (g_str_has_prefix (directory, "resource://"))
+        ide_shortcut_manager_load_resources (self, directory, cancellable);
+      else
+        ide_shortcut_manager_load_directory (self, directory, cancellable);
+    }
+
+  return TRUE;
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_shortcut_manager_initiable_init;
+}
+
+
+/**
+ * ide_shortcut_manager_get_default:
+ *
+ * Gets the singleton #IdeShortcutManager for the process.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutManager.
+ */
+IdeShortcutManager *
+ide_shortcut_manager_get_default (void)
+{
+  static IdeShortcutManager *instance;
+
+  if (instance == NULL)
+    {
+      instance = g_object_new (IDE_TYPE_SHORTCUT_MANAGER, NULL);
+      g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance);
+    }
+
+  return instance;
+}
+
+/**
+ * ide_shortcut_manager_get_theme:
+ * @self: (nullable): A #IdeShortcutManager or %NULL
+ *
+ * Gets the "theme" property.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeShortcutTheme.
+ */
+IdeShortcutTheme *
+ide_shortcut_manager_get_theme (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv;
+
+  g_return_val_if_fail (!self || IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  priv = ide_shortcut_manager_get_instance_private (self);
+
+  if (priv->theme == NULL)
+    priv->theme = g_object_new (IDE_TYPE_SHORTCUT_THEME,
+                                "name", "default",
+                                NULL);
+
+  return priv->theme;
+}
+
+/**
+ * ide_shortcut_manager_set_theme:
+ * @self: An #IdeShortcutManager
+ * @theme: (not nullable): An #IdeShortcutTheme
+ *
+ * Sets the theme for the shortcut manager.
+ */
+void
+ide_shortcut_manager_set_theme (IdeShortcutManager *self,
+                                IdeShortcutTheme   *theme)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+  /*
+   * It is important that IdeShortcutController instances watch for
+   * notify::theme so that they can reset their state. Otherwise, we
+   * could be transitioning between incorrect contexts.
+   */
+
+  if (g_set_object (&priv->theme, theme))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME_NAME]);
+    }
+}
+
+/**
+ * ide_shortcut_manager_handle_event:
+ * @self: (nullable): An #IdeShortcutManager
+ * @toplevel: A #GtkWidget or %NULL.
+ * @event: A #GdkEventKey event to handle.
+ *
+ * This function will try to dispatch @event to the proper widget and
+ * #IdeShortcutContext. If the event is handled, then %TRUE is returned.
+ *
+ * You should call this from #GtkWidget::key-press-event handler in your
+ * #GtkWindow toplevel.
+ *
+ * Returns: %TRUE if the event was handled.
+ */
+gboolean
+ide_shortcut_manager_handle_event (IdeShortcutManager *self,
+                                   const GdkEventKey  *event,
+                                   GtkWidget          *toplevel)
+{
+  GtkWidget *widget;
+  GtkWidget *focus;
+  GdkModifierType modifier;
+
+  g_return_val_if_fail (!self || IDE_IS_SHORTCUT_MANAGER (self), FALSE);
+  g_return_val_if_fail (!toplevel || GTK_IS_WINDOW (toplevel), FALSE);
+  g_return_val_if_fail (event != NULL, FALSE);
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  if (toplevel == NULL)
+    {
+      gpointer user_data;
+
+      gdk_window_get_user_data (event->window, &user_data);
+      g_return_val_if_fail (GTK_IS_WIDGET (user_data), FALSE);
+
+      toplevel = gtk_widget_get_toplevel (user_data);
+      g_return_val_if_fail (GTK_IS_WINDOW (toplevel), FALSE);
+    }
+
+  if (event->type != GDK_KEY_PRESS)
+    return GDK_EVENT_PROPAGATE;
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (GTK_IS_WINDOW (toplevel));
+  g_assert (event != NULL);
+
+  modifier = event->state & gtk_accelerator_get_default_mod_mask ();
+  widget = focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+
+  while (widget != NULL)
+    {
+      g_autoptr(GtkWidget) widget_hold = g_object_ref (widget);
+      IdeShortcutController *controller;
+      gboolean use_binding_sets = TRUE;
+
+      if (NULL != (controller = ide_shortcut_controller_find (widget)))
+        {
+          IdeShortcutContext *context = ide_shortcut_controller_get_context (controller);
+
+          /*
+           * Fetch this property first as the controller context could change
+           * during activation of the handle_event().
+           */
+          if (context != NULL)
+            g_object_get (context,
+                          "use-binding-sets", &use_binding_sets,
+                          NULL);
+
+          /*
+           * Now try to activate the event using the controller.
+           */
+          if (ide_shortcut_controller_handle_event (controller, event))
+            return GDK_EVENT_STOP;
+        }
+
+      /*
+       * If the current context at activation indicates that we can
+       * dispatch using the default binding sets for the widget, go
+       * ahead and try to do that.
+       */
+      if (use_binding_sets)
+        {
+          GtkStyleContext *style_context;
+          g_autoptr(GPtrArray) sets = NULL;
+
+          style_context = gtk_widget_get_style_context (widget);
+          gtk_style_context_get (style_context,
+                                 gtk_style_context_get_state (style_context),
+                                 "-gtk-key-bindings", &sets,
+                                 NULL);
+
+          if (sets != NULL)
+            {
+              for (guint i = 0; i < sets->len; i++)
+                {
+                  GtkBindingSet *set = g_ptr_array_index (sets, i);
+
+                  if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+                    return GDK_EVENT_STOP;
+                }
+            }
+
+          /*
+           * Only if this widget is also our focus, try to activate the default
+           * keybindings for the widget.
+           */
+          if (widget == focus)
+            {
+              GtkBindingSet *set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (widget));
+
+              if (gtk_binding_set_activate (set, event->keyval, modifier, G_OBJECT (widget)))
+                return GDK_EVENT_STOP;
+            }
+        }
+
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+const gchar *
+ide_shortcut_manager_get_theme_name (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+  const gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  if (priv->theme != NULL)
+    ret = ide_shortcut_theme_get_name (priv->theme);
+
+  return ret;
+}
+
+void
+ide_shortcut_manager_set_theme_name (IdeShortcutManager *self,
+                                     const gchar        *name)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+
+  if (name == NULL)
+    name = "default";
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      IdeShortcutTheme *theme = g_ptr_array_index (priv->themes, i);
+      const gchar *theme_name = ide_shortcut_theme_get_name (theme);
+
+      if (g_strcmp0 (name, theme_name) == 0)
+        {
+          ide_shortcut_manager_set_theme (self, theme);
+          return;
+        }
+    }
+
+  g_warning ("No such shortcut theme “%s”", name);
+}
+
+static guint
+ide_shortcut_manager_get_n_items (GListModel *model)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)model;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), 0);
+
+  return priv->themes->len;
+}
+
+static GType
+ide_shortcut_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_SHORTCUT_THEME;
+}
+
+static gpointer
+ide_shortcut_manager_get_item (GListModel *model,
+                               guint       position)
+{
+  IdeShortcutManager *self = (IdeShortcutManager *)model;
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+  g_return_val_if_fail (position < priv->themes->len, NULL);
+
+  return g_object_ref (g_ptr_array_index (priv->themes, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_shortcut_manager_get_n_items;
+  iface->get_item_type = ide_shortcut_manager_get_item_type;
+  iface->get_item = ide_shortcut_manager_get_item;
+}
+
+void
+ide_shortcut_manager_add_theme (IdeShortcutManager *self,
+                                IdeShortcutTheme   *theme)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+  guint position;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      if (g_ptr_array_index (priv->themes, i) == theme)
+        {
+          g_warning ("%s named %s has already been added",
+                     G_OBJECT_TYPE_NAME (theme),
+                     ide_shortcut_theme_get_name (theme));
+          return;
+        }
+    }
+
+  position = priv->themes->len;
+
+  g_ptr_array_add (priv->themes, g_object_ref (theme));
+
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+}
+
+void
+ide_shortcut_manager_remove_theme (IdeShortcutManager *self,
+                                   IdeShortcutTheme   *theme)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (theme));
+
+  for (guint i = 0; i < priv->themes->len; i++)
+    {
+      if (g_ptr_array_index (priv->themes, i) == theme)
+        {
+          g_ptr_array_remove_index (priv->themes, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+}
+
+const gchar *
+ide_shortcut_manager_get_user_dir (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  if (priv->user_dir == NULL)
+    {
+      priv->user_dir = g_build_filename (g_get_user_data_dir (),
+                                         g_get_prgname (),
+                                         NULL);
+    }
+
+  return priv->user_dir;
+}
+
+void
+ide_shortcut_manager_set_user_dir (IdeShortcutManager *self,
+                                   const gchar        *user_dir)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+
+  if (g_strcmp0 (user_dir, priv->user_dir) != 0)
+    {
+      g_free (priv->user_dir);
+      priv->user_dir = g_strdup (user_dir);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USER_DIR]);
+    }
+}
+
+void
+ide_shortcut_manager_append_search_path (IdeShortcutManager *self,
+                                         const gchar        *directory)
+{
+  IdeShortcutManagerPrivate *priv;
+
+  g_return_if_fail (!self || IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (directory != NULL);
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_queue_push_tail (&priv->search_path, g_strdup (directory));
+}
+
+void
+ide_shortcut_manager_prepend_search_path (IdeShortcutManager *self,
+                                          const gchar        *directory)
+{
+  IdeShortcutManagerPrivate *priv;
+
+  g_return_if_fail (!self || IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (directory != NULL);
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_queue_push_head (&priv->search_path, g_strdup (directory));
+}
+
+/**
+ * ide_shortcut_manager_get_search_path:
+ * @self: A #IdeShortcutManager
+ *
+ * This function will get the list of search path entries. These are used to
+ * load themes for the application. You should set this search path for
+ * themes before calling g_initable_init() on the search manager.
+ *
+ * Returns: (transfer none) (element-type utf8): A #GList containing each of
+ *   the search path items used to load shortcut themes.
+ */
+const GList *
+ide_shortcut_manager_get_search_path (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv;
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+
+  priv = ide_shortcut_manager_get_instance_private (self);
+
+  return priv->search_path.head;
+}
+
+static GNode *
+ide_shortcut_manager_find_child (IdeShortcutManager  *self,
+                                 GNode               *parent,
+                                 IdeShortcutNodeType  type,
+                                 const gchar         *name)
+{
+  IdeShortcutNodeData *data;
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (parent != NULL);
+  g_assert (type != 0);
+  g_assert (name != NULL);
+
+  for (GNode *iter = parent->children; iter != NULL; iter = iter->next)
+    {
+      data = iter->data;
+
+      if (data->type == type && data->name == name)
+        return iter;
+    }
+
+  return NULL;
+}
+
+static GNode *
+ide_shortcut_manager_get_group (IdeShortcutManager *self,
+                                const gchar        *section,
+                                const gchar        *group)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+  IdeShortcutNodeData *data;
+  GNode *parent;
+  GNode *node;
+
+  g_assert (IDE_IS_SHORTCUT_MANAGER (self));
+  g_assert (section != NULL);
+  g_assert (group != NULL);
+
+  node = ide_shortcut_manager_find_child (self, priv->root, IDE_SHORTCUT_NODE_SECTION, section);
+
+  if (node == NULL)
+    {
+      data = g_slice_new0 (IdeShortcutNodeData);
+      data->type = IDE_SHORTCUT_NODE_SECTION;
+      data->name = g_intern_string (section);
+      data->title = g_intern_string (section);
+      data->subtitle = NULL;
+
+      node = g_node_append_data (priv->root, data);
+    }
+
+  parent = node;
+
+  node = ide_shortcut_manager_find_child (self, parent, IDE_SHORTCUT_NODE_GROUP, group);
+
+  if (node == NULL)
+    {
+      data = g_slice_new0 (IdeShortcutNodeData);
+      data->type = IDE_SHORTCUT_NODE_GROUP;
+      data->name = g_intern_string (group);
+      data->title = g_intern_string (group);
+      data->subtitle = NULL;
+
+      node = g_node_append_data (parent, data);
+    }
+
+  g_assert (node != NULL);
+
+  return node;
+}
+
+void
+ide_shortcut_manager_add_action (IdeShortcutManager *self,
+                                 const gchar        *detailed_action_name,
+                                 const gchar        *section,
+                                 const gchar        *group,
+                                 const gchar        *title,
+                                 const gchar        *subtitle)
+{
+  IdeShortcutNodeData *data;
+  GNode *parent;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (detailed_action_name != NULL);
+  g_return_if_fail (title != NULL);
+
+  section = g_intern_string (section);
+  group = g_intern_string (group);
+  title = g_intern_string (title);
+  subtitle = g_intern_string (subtitle);
+
+  parent = ide_shortcut_manager_get_group (self, section, group);
+
+  g_assert (parent != NULL);
+
+  data = g_slice_new0 (IdeShortcutNodeData);
+  data->type = IDE_SHORTCUT_NODE_ACTION;
+  data->name = g_intern_string (detailed_action_name);
+  data->title = title;
+  data->subtitle = subtitle;
+
+  g_node_append_data (parent, data);
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+void
+ide_shortcut_manager_add_command (IdeShortcutManager *self,
+                                  const gchar        *command,
+                                  const gchar        *section,
+                                  const gchar        *group,
+                                  const gchar        *title,
+                                  const gchar        *subtitle)
+{
+  IdeShortcutNodeData *data;
+  GNode *parent;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (command != NULL);
+  g_return_if_fail (title != NULL);
+
+  section = g_intern_string (section);
+  group = g_intern_string (group);
+  title = g_intern_string (title);
+  subtitle = g_intern_string (subtitle);
+
+  parent = ide_shortcut_manager_get_group (self, section, group);
+
+  g_assert (parent != NULL);
+
+  data = g_slice_new0 (IdeShortcutNodeData);
+  data->type = IDE_SHORTCUT_NODE_COMMAND;
+  data->name = g_intern_string (command);
+  data->title = title;
+  data->subtitle = subtitle;
+
+  g_node_append_data (parent, data);
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static IdeShortcutsShortcut *
+create_shortcut (const IdeShortcutChord *chord,
+                 const gchar            *title,
+                 const gchar            *subtitle)
+{
+  g_autofree gchar *accel = ide_shortcut_chord_to_string (chord);
+
+  return g_object_new (IDE_TYPE_SHORTCUTS_SHORTCUT,
+                       "accelerator", accel,
+                       "subtitle", subtitle,
+                       "title", title,
+                       "visible", TRUE,
+                       NULL);
+}
+
+/**
+ * ide_shortcut_manager_add_shortcuts_to_window:
+ * @self: A #IdeShortcutManager
+ * @window: A #IdeShortcutsWindow
+ *
+ * Adds shortcuts registered with the #IdeShortcutManager to the
+ * #IdeShortcutsWindow.
+ */
+void
+ide_shortcut_manager_add_shortcuts_to_window (IdeShortcutManager *self,
+                                              IdeShortcutsWindow *window)
+{
+  IdeShortcutManagerPrivate *priv;
+  GNode *parent;
+
+  g_return_if_fail (!self || IDE_IS_SHORTCUT_MANAGER (self));
+  g_return_if_fail (IDE_IS_SHORTCUTS_WINDOW (window));
+
+  if (self == NULL)
+    self = ide_shortcut_manager_get_default ();
+  priv = ide_shortcut_manager_get_instance_private (self);
+
+  /*
+   * The GNode tree is in four levels. priv->root is the root of the tree and
+   * contains no data items itself. It is just our stable root. The children
+   * of priv->root are our section nodes. Each section node has group nodes
+   * as children. Finally, the shortcut nodes are the leaves.
+   */
+
+  parent = priv->root;
+
+  for (const GNode *sections = parent->children; sections != NULL; sections = sections->next)
+    {
+      IdeShortcutNodeData *section_data = sections->data;
+      IdeShortcutsSection *section;
+
+      section = g_object_new (IDE_TYPE_SHORTCUTS_SECTION,
+                              "title", section_data->title,
+                              "section-name", section_data->title,
+                              "visible", TRUE,
+                              NULL);
+
+      for (const GNode *groups = sections->children; groups != NULL; groups = groups->next)
+        {
+          IdeShortcutNodeData *group_data = groups->data;
+          IdeShortcutsGroup *group;
+
+          group = g_object_new (IDE_TYPE_SHORTCUTS_GROUP,
+                                "title", group_data->title,
+                                "visible", TRUE,
+                                NULL);
+
+          for (const GNode *iter = groups->children; iter != NULL; iter = iter->next)
+            {
+              IdeShortcutNodeData *data = iter->data;
+              IdeShortcutsShortcut *shortcut;
+              const IdeShortcutChord *chord;
+              g_autofree gchar *accel = NULL;
+
+              if (data->type == IDE_SHORTCUT_NODE_ACTION)
+                chord = ide_shortcut_theme_get_chord_for_action (priv->theme, data->name);
+              else
+                chord = ide_shortcut_theme_get_chord_for_command (priv->theme, data->name);
+
+              accel = ide_shortcut_chord_to_string (chord);
+
+              shortcut = create_shortcut (chord, data->title, data->subtitle);
+              gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (shortcut));
+            }
+
+          gtk_container_add (GTK_CONTAINER (section), GTK_WIDGET (group));
+        }
+
+      gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (section));
+    }
+}
+
+GNode *
+_ide_shortcut_manager_get_root (IdeShortcutManager *self)
+{
+  IdeShortcutManagerPrivate *priv = ide_shortcut_manager_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MANAGER (self), NULL);
+
+  return priv->root;
+}
diff --git a/libide/shortcuts/ide-shortcut-manager.h b/libide/shortcuts/ide-shortcut-manager.h
new file mode 100644
index 0000000..eeaec24
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-manager.h
@@ -0,0 +1,85 @@
+/* ide-shortcut-manager.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 2 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 IDE_SHORTCUT_MANAGER_H
+#define IDE_SHORTCUT_MANAGER_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-theme.h"
+#include "ide-shortcuts-window.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_MANAGER (ide_shortcut_manager_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeShortcutManager, ide_shortcut_manager, IDE, SHORTCUT_MANAGER, GObject)
+
+struct _IdeShortcutManagerClass
+{
+  GObjectClass parent_instance;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+IdeShortcutManager *ide_shortcut_manager_get_default             (void);
+void                ide_shortcut_manager_append_search_path      (IdeShortcutManager *self,
+                                                                  const gchar        *directory);
+void                ide_shortcut_manager_prepend_search_path     (IdeShortcutManager *self,
+                                                                  const gchar        *directory);
+IdeShortcutTheme   *ide_shortcut_manager_get_theme               (IdeShortcutManager *self);
+void                ide_shortcut_manager_set_theme               (IdeShortcutManager *self,
+                                                                  IdeShortcutTheme   *theme);
+const gchar        *ide_shortcut_manager_get_theme_name          (IdeShortcutManager *self);
+void                ide_shortcut_manager_set_theme_name          (IdeShortcutManager *self,
+                                                                  const gchar        *theme_name);
+gboolean            ide_shortcut_manager_handle_event            (IdeShortcutManager *self,
+                                                                  const GdkEventKey  *event,
+                                                                  GtkWidget          *toplevel);
+void                ide_shortcut_manager_add_theme               (IdeShortcutManager *self,
+                                                                  IdeShortcutTheme   *theme);
+void                ide_shortcut_manager_remove_theme            (IdeShortcutManager *self,
+                                                                  IdeShortcutTheme   *theme);
+const gchar        *ide_shortcut_manager_get_user_dir            (IdeShortcutManager *self);
+void                ide_shortcut_manager_set_user_dir            (IdeShortcutManager *self,
+                                                                  const gchar        *user_dir);
+void                ide_shortcut_manager_add_action              (IdeShortcutManager *self,
+                                                                  const gchar        *detailed_action_name,
+                                                                  const gchar        *section,
+                                                                  const gchar        *group,
+                                                                  const gchar        *title,
+                                                                  const gchar        *subtitle);
+void                ide_shortcut_manager_add_command             (IdeShortcutManager *self,
+                                                                  const gchar        *command,
+                                                                  const gchar        *section,
+                                                                  const gchar        *group,
+                                                                  const gchar        *title,
+                                                                  const gchar        *subtitle);
+void                ide_shortcut_manager_add_shortcuts_to_window (IdeShortcutManager *self,
+                                                                  IdeShortcutsWindow *window);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_MANAGER_H */
diff --git a/libide/shortcuts/ide-shortcut-model.c b/libide/shortcuts/ide-shortcut-model.c
new file mode 100644
index 0000000..e21b4ad
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-model.c
@@ -0,0 +1,331 @@
+/* ide-shortcut-model.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-model"
+
+#include "ide-shortcut-model.h"
+#include "ide-shortcut-private.h"
+
+struct _IdeShortcutModel
+{
+  GtkTreeStore        parent_instance;
+  IdeShortcutManager *manager;
+  IdeShortcutTheme   *theme;
+};
+
+G_DEFINE_TYPE (IdeShortcutModel, ide_shortcut_model, GTK_TYPE_TREE_STORE)
+
+enum {
+  PROP_0,
+  PROP_MANAGER,
+  PROP_THEME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+void
+ide_shortcut_model_rebuild (IdeShortcutModel *self)
+{
+  g_assert (IDE_IS_SHORTCUT_MODEL (self));
+
+  gtk_tree_store_clear (GTK_TREE_STORE (self));
+
+  if (self->manager != NULL)
+    {
+      GNode *root;
+
+      root = _ide_shortcut_manager_get_root (self->manager);
+
+      for (const GNode *iter = root->children; iter != NULL; iter = iter->next)
+        {
+          for (const GNode *groups = iter->children; groups != NULL; groups = groups->next)
+            {
+              IdeShortcutNodeData *group = groups->data;
+              GtkTreeIter p;
+
+              gtk_tree_store_append (GTK_TREE_STORE (self), &p, NULL);
+              gtk_tree_store_set (GTK_TREE_STORE (self), &p,
+                                  IDE_SHORTCUT_MODEL_COLUMN_TITLE, group->title,
+                                  -1);
+
+              for (const GNode *sc = groups->children; sc != NULL; sc = sc->next)
+                {
+                  IdeShortcutNodeData *shortcut = sc->data;
+                  const IdeShortcutChord *chord = NULL;
+                  g_autofree gchar *accel = NULL;
+                  g_autofree gchar *down = NULL;
+                  GtkTreeIter p2;
+
+                  if (shortcut->type == IDE_SHORTCUT_NODE_ACTION)
+                    chord = ide_shortcut_theme_get_chord_for_action (self->theme, shortcut->name);
+                  else if (shortcut->type == IDE_SHORTCUT_NODE_COMMAND)
+                    chord = ide_shortcut_theme_get_chord_for_command (self->theme, shortcut->name);
+
+                  accel = ide_shortcut_chord_get_label (chord);
+                  down = g_utf8_casefold (shortcut->title, -1);
+
+                  gtk_tree_store_append (GTK_TREE_STORE (self), &p2, &p);
+                  gtk_tree_store_set (GTK_TREE_STORE (self), &p2,
+                                      IDE_SHORTCUT_MODEL_COLUMN_TYPE, shortcut->type,
+                                      IDE_SHORTCUT_MODEL_COLUMN_ID, shortcut->name,
+                                      IDE_SHORTCUT_MODEL_COLUMN_TITLE, shortcut->title,
+                                      IDE_SHORTCUT_MODEL_COLUMN_ACCEL, accel,
+                                      IDE_SHORTCUT_MODEL_COLUMN_KEYWORDS, down,
+                                      IDE_SHORTCUT_MODEL_COLUMN_CHORD, chord,
+                                      -1);
+                }
+            }
+        }
+    }
+}
+
+static void
+ide_shortcut_model_constructed (GObject *object)
+{
+  IdeShortcutModel *self = (IdeShortcutModel *)object;
+
+  g_assert (IDE_IS_SHORTCUT_MODEL (self));
+
+  G_OBJECT_CLASS (ide_shortcut_model_parent_class)->constructed (object);
+
+  ide_shortcut_model_rebuild (self);
+}
+
+static void
+ide_shortcut_model_finalize (GObject *object)
+{
+  IdeShortcutModel *self = (IdeShortcutModel *)object;
+
+  g_clear_object (&self->manager);
+  g_clear_object (&self->theme);
+
+  G_OBJECT_CLASS (ide_shortcut_model_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_model_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutModel *self = IDE_SHORTCUT_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MANAGER:
+      g_value_set_object (value, ide_shortcut_model_get_manager (self));
+      break;
+
+    case PROP_THEME:
+      g_value_set_object (value, ide_shortcut_model_get_theme (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_model_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutModel *self = IDE_SHORTCUT_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_MANAGER:
+      ide_shortcut_model_set_manager (self, g_value_get_object (value));
+      break;
+
+    case PROP_THEME:
+      ide_shortcut_model_set_theme (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_model_class_init (IdeShortcutModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_shortcut_model_constructed;
+  object_class->finalize = ide_shortcut_model_finalize;
+  object_class->get_property = ide_shortcut_model_get_property;
+  object_class->set_property = ide_shortcut_model_set_property;
+
+  properties [PROP_MANAGER] =
+    g_param_spec_object ("manager",
+                         "Manager",
+                         "Manager",
+                         IDE_TYPE_SHORTCUT_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_THEME] =
+    g_param_spec_object ("theme",
+                         "Theme",
+                         "Theme",
+                         IDE_TYPE_SHORTCUT_THEME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_model_init (IdeShortcutModel *self)
+{
+  GType element_types[] = {
+    G_TYPE_INT,
+    G_TYPE_STRING,
+    G_TYPE_STRING,
+    G_TYPE_STRING,
+    G_TYPE_STRING,
+    IDE_TYPE_SHORTCUT_CHORD,
+  };
+
+  G_STATIC_ASSERT (G_N_ELEMENTS (element_types) == IDE_SHORTCUT_MODEL_N_COLUMNS);
+
+  self->manager = g_object_ref (ide_shortcut_manager_get_default ());
+
+  gtk_tree_store_set_column_types (GTK_TREE_STORE (self),
+                                   G_N_ELEMENTS (element_types),
+                                   element_types);
+}
+
+/**
+ * ide_shortcut_model_new:
+ *
+ * Returns: (transfer full): A #GtkTreeModel
+ */
+GtkTreeModel *
+ide_shortcut_model_new (void)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_MODEL, NULL);
+}
+
+/**
+ * ide_shortcut_model_get_manager:
+ * @self: a #IdeShortcutModel
+ *
+ * Gets the manager to be edited.
+ *
+ * Returns: (transfer none): A #IdeShortcutManager
+ */
+IdeShortcutManager *
+ide_shortcut_model_get_manager (IdeShortcutModel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MODEL (self), NULL);
+
+  return self->manager;
+}
+
+void
+ide_shortcut_model_set_manager (IdeShortcutModel   *self,
+                                IdeShortcutManager *manager)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_MODEL (self));
+  g_return_if_fail (!manager || IDE_IS_SHORTCUT_MANAGER (manager));
+
+  if (g_set_object (&self->manager, manager))
+    {
+      ide_shortcut_model_rebuild (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MANAGER]);
+    }
+}
+
+/**
+ * ide_shortcut_model_get_theme:
+ * @self: a #IdeShortcutModel
+ *
+ * Get the theme to be edited.
+ *
+ * Returns: (transfer none): A #IdeShortcutTheme
+ */
+IdeShortcutTheme *
+ide_shortcut_model_get_theme (IdeShortcutModel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_MODEL (self), NULL);
+
+  return self->theme;
+}
+
+void
+ide_shortcut_model_set_theme (IdeShortcutModel *self,
+                              IdeShortcutTheme *theme)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_MODEL (self));
+  g_return_if_fail (!theme || IDE_IS_SHORTCUT_THEME (theme));
+
+  if (g_set_object (&self->theme, theme))
+    {
+      ide_shortcut_model_rebuild (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
+    }
+}
+
+static void
+ide_shortcut_model_apply (IdeShortcutModel *self,
+                          GtkTreeIter      *iter)
+{
+  g_autoptr(IdeShortcutChord) chord = NULL;
+  g_autofree gchar *id = NULL;
+  gint type = 0;
+
+  g_assert (IDE_IS_SHORTCUT_MODEL (self));
+  g_assert (IDE_IS_SHORTCUT_THEME (self->theme));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get (GTK_TREE_MODEL (self), iter,
+                      IDE_SHORTCUT_MODEL_COLUMN_TYPE, &type,
+                      IDE_SHORTCUT_MODEL_COLUMN_ID, &id,
+                      IDE_SHORTCUT_MODEL_COLUMN_CHORD, &chord,
+                      -1);
+
+  if (type == IDE_SHORTCUT_NODE_ACTION)
+    ide_shortcut_theme_set_chord_for_action (self->theme, id, chord);
+  else if (type == IDE_SHORTCUT_NODE_COMMAND)
+    ide_shortcut_theme_set_chord_for_command (self->theme, id, chord);
+  else
+    g_warning ("Unknown type: %d", type);
+}
+
+void
+ide_shortcut_model_set_chord (IdeShortcutModel       *self,
+                              GtkTreeIter            *iter,
+                              const IdeShortcutChord *chord)
+{
+  g_autofree gchar *accel = NULL;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_MODEL (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (gtk_tree_store_iter_is_valid (GTK_TREE_STORE (self), iter));
+
+  accel = ide_shortcut_chord_get_label (chord);
+
+  gtk_tree_store_set (GTK_TREE_STORE (self), iter,
+                      IDE_SHORTCUT_MODEL_COLUMN_ACCEL, accel,
+                      IDE_SHORTCUT_MODEL_COLUMN_CHORD, chord,
+                      -1);
+
+  ide_shortcut_model_apply (self, iter);
+}
diff --git a/libide/shortcuts/ide-shortcut-model.h b/libide/shortcuts/ide-shortcut-model.h
new file mode 100644
index 0000000..1dc684b
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-model.h
@@ -0,0 +1,48 @@
+/* ide-shortcut-model.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_MODEL_H
+#define IDE_SHORTCUT_MODEL_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-manager.h"
+#include "ide-shortcut-theme.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_MODEL (ide_shortcut_model_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutModel, ide_shortcut_model, IDE, SHORTCUT_MODEL, GtkTreeStore)
+
+GtkTreeModel       *ide_shortcut_model_new         (void);
+IdeShortcutManager *ide_shortcut_model_get_manager (IdeShortcutModel       *self);
+void                ide_shortcut_model_set_manager (IdeShortcutModel       *self,
+                                                    IdeShortcutManager     *manager);
+IdeShortcutTheme   *ide_shortcut_model_get_theme   (IdeShortcutModel       *self);
+void                ide_shortcut_model_set_theme   (IdeShortcutModel       *self,
+                                                    IdeShortcutTheme       *theme);
+void                ide_shortcut_model_set_chord   (IdeShortcutModel       *self,
+                                                    GtkTreeIter            *iter,
+                                                    const IdeShortcutChord *chord);
+void                ide_shortcut_model_rebuild     (IdeShortcutModel       *self);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_MODEL_H */
diff --git a/libide/shortcuts/ide-shortcut-private.h b/libide/shortcuts/ide-shortcut-private.h
new file mode 100644
index 0000000..8a6895e
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-private.h
@@ -0,0 +1,97 @@
+/* ide-shortcut-theme-private.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_THEME_PRIVATE_H
+#define IDE_SHORTCUT_THEME_PRIVATE_H
+
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-manager.h"
+#include "ide-shortcut-theme.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  IdeShortcutChordTable *table;
+  guint                  position;
+} IdeShortcutChordTableIter;
+
+typedef enum
+{
+  IDE_SHORTCUT_NODE_SECTION = 1,
+  IDE_SHORTCUT_NODE_GROUP,
+  IDE_SHORTCUT_NODE_ACTION,
+  IDE_SHORTCUT_NODE_COMMAND,
+} IdeShortcutNodeType;
+
+typedef struct
+{
+  IdeShortcutNodeType  type;
+  const gchar         *name;
+  const gchar         *title;
+  const gchar         *subtitle;
+} IdeShortcutNodeData;
+
+typedef enum
+{
+  SHORTCUT_ACTION = 1,
+  SHORTCUT_SIGNAL,
+} ShortcutType;
+
+typedef struct _Shortcut
+{
+  ShortcutType      type;
+  union {
+    struct {
+      const gchar  *prefix;
+      const gchar  *name;
+      GVariant     *param;
+    } action;
+    struct {
+      const gchar  *name;
+      GQuark        detail;
+      GArray       *params;
+    } signal;
+  };
+  struct _Shortcut *next;
+} Shortcut;
+
+typedef enum
+{
+  IDE_SHORTCUT_MODEL_COLUMN_TYPE,
+  IDE_SHORTCUT_MODEL_COLUMN_ID,
+  IDE_SHORTCUT_MODEL_COLUMN_TITLE,
+  IDE_SHORTCUT_MODEL_COLUMN_ACCEL,
+  IDE_SHORTCUT_MODEL_COLUMN_KEYWORDS,
+  IDE_SHORTCUT_MODEL_COLUMN_CHORD,
+  IDE_SHORTCUT_MODEL_N_COLUMNS
+} IdeShortcutModelColumn;
+
+GNode                 *_ide_shortcut_manager_get_root      (IdeShortcutManager         *self);
+GtkTreeModel          *_ide_shortcut_theme_create_model    (IdeShortcutTheme           *self);
+GHashTable            *_ide_shortcut_theme_get_contexts    (IdeShortcutTheme           *self);
+IdeShortcutChordTable *_ide_shortcut_context_get_table     (IdeShortcutContext         *self);
+void                   _ide_shortcut_chord_table_iter_init (IdeShortcutChordTableIter  *iter,
+                                                            IdeShortcutChordTable      *table);
+gboolean               _ide_shortcut_chord_table_iter_next (IdeShortcutChordTableIter  *iter,
+                                                            const IdeShortcutChord    **chord,
+                                                            gpointer                   *value);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_THEME_PRIVATE_H */
diff --git a/libide/shortcuts/ide-shortcut-theme-editor.c b/libide/shortcuts/ide-shortcut-theme-editor.c
new file mode 100644
index 0000000..361b44d
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme-editor.c
@@ -0,0 +1,477 @@
+/* ide-shortcut-theme-editor.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-theme-editor"
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcut-accel-dialog.h"
+#include "ide-shortcut-model.h"
+#include "ide-shortcut-private.h"
+#include "ide-shortcut-theme-editor.h"
+
+typedef struct
+{
+  GtkTreeView         *tree_view;
+  GtkSearchEntry      *filter_entry;
+  GtkTreeViewColumn   *shortcut_column;
+  GtkCellRendererText *shortcut_cell;
+  GtkTreeViewColumn   *title_column;
+  GtkCellRendererText *title_cell;
+
+  IdeShortcutTheme    *theme;
+  GtkTreeModel        *model;
+  GtkTreePath         *selected;
+  PangoAttrList       *attrs;
+} IdeShortcutThemeEditorPrivate;
+
+enum {
+  PROP_0,
+  PROP_THEME,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutThemeEditor, ide_shortcut_theme_editor, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_shortcut_theme_editor_dialog_response (IdeShortcutThemeEditor *self,
+                                           gint                    response_code,
+                                           IdeShortcutAccelDialog *dialog)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+  gboolean changed = FALSE;
+
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+  g_assert (IDE_SHORTCUT_ACCEL_DIALOG (dialog));
+
+  if (response_code == GTK_RESPONSE_ACCEPT)
+    {
+      const IdeShortcutChord *chord = ide_shortcut_accel_dialog_get_chord (dialog);
+
+      if (priv->selected != NULL)
+        {
+          GtkTreePath *path;
+          GtkTreeModel *model;
+          GtkTreeIter iter;
+
+          model = gtk_tree_view_get_model (priv->tree_view);
+
+          if (GTK_IS_TREE_STORE (model))
+            path = gtk_tree_path_copy (priv->selected);
+          else
+            path = gtk_tree_model_filter_convert_path_to_child_path (GTK_TREE_MODEL_FILTER (model), 
priv->selected);
+
+          if (gtk_tree_model_get_iter (model, &iter, path))
+            ide_shortcut_model_set_chord (IDE_SHORTCUT_MODEL (priv->model), &iter, chord);
+        }
+
+      changed = TRUE;
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+
+  if (changed)
+    g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static gboolean
+ide_shortcut_theme_editor_visible_func (GtkTreeModel *model,
+                                        GtkTreeIter  *iter,
+                                        gpointer      user_data)
+{
+  const gchar *text = user_data;
+  g_autofree gchar *keywords= NULL;
+  GtkTreeIter parent;
+
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (text != NULL);
+
+  if (!gtk_tree_model_iter_parent (model, &parent, iter))
+    return TRUE;
+
+  gtk_tree_model_get (model, iter,
+                      IDE_SHORTCUT_MODEL_COLUMN_KEYWORDS, &keywords,
+                      -1);
+
+  /* keywords and text are both casefolded */
+  if (strstr (keywords, text) != NULL)
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+ide_shortcut_theme_editor_filter_changed (IdeShortcutThemeEditor *self,
+                                          GtkSearchEntry         *entry)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+  g_autoptr(GtkTreeModel) filter = NULL;
+  const gchar *text;
+
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
+
+  filter = gtk_tree_model_filter_new (priv->model, NULL);
+  text = gtk_entry_get_text (GTK_ENTRY (entry));
+
+  if (!text || !*text)
+    {
+      gtk_tree_view_set_model (priv->tree_view, priv->model);
+      gtk_tree_view_expand_all (priv->tree_view);
+      return;
+    }
+
+  gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
+                                          ide_shortcut_theme_editor_visible_func,
+                                          text ? g_utf8_casefold (text, -1) : NULL,
+                                          g_free);
+  gtk_tree_view_set_model (priv->tree_view, GTK_TREE_MODEL (filter));
+  gtk_tree_view_expand_all (priv->tree_view);
+}
+
+static void
+ide_shortcut_theme_editor_row_activated (IdeShortcutThemeEditor *self,
+                                         GtkTreePath            *tree_path,
+                                         GtkTreeViewColumn      *column,
+                                         GtkTreeView            *tree_view)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+  g_assert (tree_path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  if (gtk_tree_path_get_depth (tree_path) == 1)
+    return;
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (gtk_tree_model_get_iter (model, &iter, tree_path))
+    {
+      g_autofree gchar *title = NULL;
+      g_autofree gchar *accel = NULL;
+      GtkDialog *dialog;
+      GtkWidget *toplevel;
+
+      g_clear_pointer (&priv->selected, gtk_tree_path_free);
+      priv->selected = gtk_tree_path_copy (tree_path);
+
+      gtk_tree_model_get (model, &iter,
+                          IDE_SHORTCUT_MODEL_COLUMN_TITLE, &title,
+                          IDE_SHORTCUT_MODEL_COLUMN_ACCEL, &accel,
+                          -1);
+
+      toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+
+      dialog = g_object_new (IDE_TYPE_SHORTCUT_ACCEL_DIALOG,
+                             "modal", TRUE,
+                             "resizable", FALSE,
+                             "shortcut-title", title,
+                             "title", _("Set Shortcut"),
+                             "transient-for", toplevel,
+                             "use-header-bar", TRUE,
+                             NULL);
+
+      g_signal_connect_object (dialog,
+                               "response",
+                               G_CALLBACK (ide_shortcut_theme_editor_dialog_response),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      gtk_window_present (GTK_WINDOW (dialog));
+    }
+}
+
+static void
+shortcut_cell_data_func (GtkCellLayout   *cell_layout,
+                         GtkCellRenderer *renderer,
+                         GtkTreeModel    *model,
+                         GtkTreeIter     *iter,
+                         gpointer         user_data)
+{
+  IdeShortcutThemeEditor *self = user_data;
+  g_autofree gchar *accel = NULL;
+  GtkTreeIter piter;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER (renderer));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+
+  gtk_tree_model_get (model, iter,
+                      IDE_SHORTCUT_MODEL_COLUMN_ACCEL, &accel,
+                      -1);
+
+  if (accel && *accel)
+    g_object_set (renderer, "text", accel, NULL);
+  else if (gtk_tree_model_iter_parent (model, &piter, iter))
+    g_object_set (renderer, "text", "Disabled", NULL);
+  else
+    g_object_set (renderer, "text", NULL, NULL);
+}
+
+static void
+title_cell_data_func (GtkCellLayout   *cell_layout,
+                      GtkCellRenderer *renderer,
+                      GtkTreeModel    *model,
+                      GtkTreeIter     *iter,
+                      gpointer         user_data)
+{
+  IdeShortcutThemeEditor *self = user_data;
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+  g_autofree gchar *title = NULL;
+  GtkTreeIter piter;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER (renderer));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+
+  gtk_tree_model_get (model, iter,
+                      IDE_SHORTCUT_MODEL_COLUMN_TITLE, &title,
+                      -1);
+
+  g_object_set (renderer, "text", title, NULL);
+
+  if (!gtk_tree_model_iter_parent (model, &piter, iter))
+    g_object_set (renderer, "attributes", priv->attrs, NULL);
+  else
+    g_object_set (renderer, "attributes", NULL, NULL);
+}
+
+static void
+ide_shortcut_theme_editor_changed (IdeShortcutThemeEditor *self,
+                                   IdeShortcutManager     *manager)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+
+  g_assert (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+  g_assert (IDE_IS_SHORTCUT_MANAGER (manager));
+
+  ide_shortcut_model_rebuild (IDE_SHORTCUT_MODEL (priv->model));
+  gtk_tree_view_expand_all (priv->tree_view);
+}
+
+static void
+ide_shortcut_theme_editor_finalize (GObject *object)
+{
+  IdeShortcutThemeEditor *self = (IdeShortcutThemeEditor *)object;
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+
+  g_clear_object (&priv->model);
+  g_clear_object (&priv->theme);
+  g_clear_pointer (&priv->selected, gtk_tree_path_free);
+  g_clear_pointer (&priv->attrs, pango_attr_list_unref);
+
+  G_OBJECT_CLASS (ide_shortcut_theme_editor_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_theme_editor_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeShortcutThemeEditor *self = IDE_SHORTCUT_THEME_EDITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      g_value_set_object (value, ide_shortcut_theme_editor_get_theme (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_editor_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeShortcutThemeEditor *self = IDE_SHORTCUT_THEME_EDITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_THEME:
+      ide_shortcut_theme_editor_set_theme (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_editor_class_init (IdeShortcutThemeEditorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_theme_editor_finalize;
+  object_class->get_property = ide_shortcut_theme_editor_get_property;
+  object_class->set_property = ide_shortcut_theme_editor_set_property;
+
+  properties [PROP_THEME] =
+    g_param_spec_object ("theme",
+                         "Theme",
+                         "The theme for editing",
+                         IDE_TYPE_SHORTCUT_THEME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeShortcutThemeEditor::changed:
+   *
+   * The "changed" signal is emitted when one of the rows within the editor
+   * has been changed.
+   *
+   * You might want to use this signal to save your theme changes to your
+   * configured storage backend.
+   */
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-shortcut-theme-editor.ui");
+
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, tree_view);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, filter_entry);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, shortcut_cell);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, shortcut_column);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, title_cell);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeShortcutThemeEditor, title_column);
+}
+
+static void
+ide_shortcut_theme_editor_init (IdeShortcutThemeEditor *self)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+  PangoAttrList *list = NULL;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  priv->model = ide_shortcut_model_new ();
+  gtk_tree_view_set_model (priv->tree_view, priv->model);
+
+  g_signal_connect_object (ide_shortcut_manager_get_default (),
+                           "changed",
+                           G_CALLBACK (ide_shortcut_theme_editor_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->filter_entry,
+                           "changed",
+                           G_CALLBACK (ide_shortcut_theme_editor_filter_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->tree_view,
+                           "row-activated",
+                           G_CALLBACK (ide_shortcut_theme_editor_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Set "dim-label" like alpha on the shortcut label */
+  list = pango_attr_list_new ();
+  pango_attr_list_insert (list, pango_attr_foreground_alpha_new (0.55 * G_MAXUSHORT));
+  g_object_set (priv->shortcut_cell,
+                "attributes", list,
+                NULL);
+  pango_attr_list_unref (list);
+
+  /* Setup cell data funcs */
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->title_column),
+                                      GTK_CELL_RENDERER (priv->title_cell),
+                                      title_cell_data_func,
+                                      self,
+                                      NULL);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (priv->shortcut_column),
+                                      GTK_CELL_RENDERER (priv->shortcut_cell),
+                                      shortcut_cell_data_func,
+                                      self,
+                                      NULL);
+
+  /* diable selections on the treeview */
+  gtk_tree_selection_set_mode (gtk_tree_view_get_selection (priv->tree_view),
+                               GTK_SELECTION_NONE);
+
+  priv->attrs = pango_attr_list_new ();
+  pango_attr_list_insert (priv->attrs, pango_attr_foreground_alpha_new (0.55 * 0xFFFF));
+}
+
+GtkWidget *
+ide_shortcut_theme_editor_new (void)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_THEME_EDITOR, NULL);
+}
+
+/**
+ * ide_shortcut_theme_editor_get_theme:
+ * @self: a #IdeShortcutThemeEditor
+ *
+ * Gets the shortcut theme if one hsa been set.
+ *
+ * Returns: (transfer none) (nullable): An #IdeShortcutTheme or %NULL
+ */
+IdeShortcutTheme *
+ide_shortcut_theme_editor_get_theme (IdeShortcutThemeEditor *self)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME_EDITOR (self), NULL);
+
+  return priv->theme;
+}
+
+void
+ide_shortcut_theme_editor_set_theme (IdeShortcutThemeEditor *self,
+                                     IdeShortcutTheme       *theme)
+{
+  IdeShortcutThemeEditorPrivate *priv = ide_shortcut_theme_editor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME_EDITOR (self));
+  g_return_if_fail (!theme || IDE_IS_SHORTCUT_THEME (theme));
+
+  if (g_set_object (&priv->theme, theme))
+    {
+      ide_shortcut_model_set_theme (IDE_SHORTCUT_MODEL (priv->model), theme);
+      gtk_tree_view_expand_all (priv->tree_view);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_THEME]);
+    }
+}
diff --git a/libide/shortcuts/ide-shortcut-theme-editor.h b/libide/shortcuts/ide-shortcut-theme-editor.h
new file mode 100644
index 0000000..5ebdb34
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme-editor.h
@@ -0,0 +1,53 @@
+/* ide-shortcut-theme-editor.h
+ *
+ * Copyright (C) 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 2 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 IDE_SHORTCUT_THEME_EDITOR_H
+#define IDE_SHORTCUT_THEME_EDITOR_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-theme.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_THEME_EDITOR (ide_shortcut_theme_editor_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeShortcutThemeEditor, ide_shortcut_theme_editor, IDE, SHORTCUT_THEME_EDITOR, 
GtkBin)
+
+struct _IdeShortcutThemeEditorClass
+{
+  GtkBinClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+GtkWidget        *ide_shortcut_theme_editor_new       (void);
+IdeShortcutTheme *ide_shortcut_theme_editor_get_theme (IdeShortcutThemeEditor *self);
+void              ide_shortcut_theme_editor_set_theme (IdeShortcutThemeEditor *self,
+                                                       IdeShortcutTheme       *theme);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_THEME_EDITOR_H */
diff --git a/libide/shortcuts/ide-shortcut-theme-editor.ui b/libide/shortcuts/ide-shortcut-theme-editor.ui
new file mode 100644
index 0000000..917fde4
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme-editor.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GseShortcutThemeEditor" parent="GtkBin">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">12</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Keyboard Shortcuts</property>
+                <property name="xalign">0.0</property>
+                <property name="visible">true</property>
+                <attributes>
+                  <attribute name="weight" value="bold"/>
+                </attributes>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSearchEntry" id="filter_entry">
+                <property name="placeholder-text" translatable="yes">Filter Shortcuts</property>
+                <property name="max-width-chars">25</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="shadow-type">in</property>
+            <property name="vexpand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkTreeView" id="tree_view">
+                <property name="activate-on-single-click">true</property>
+                <property name="fixed-height-mode">true</property>
+                <property name="headers-visible">false</property>
+                <property name="show-expanders">false</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkTreeViewColumn" id="title_column">
+                    <property name="expand">true</property>
+                    <property name="sizing">fixed</property>
+                    <property name="title">Action</property>
+                    <child>
+                      <object class="GtkCellRendererText" id="title_cell">
+                        <property name="xpad">8</property>
+                        <property name="ypad">6</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkTreeViewColumn" id="shortcut_column">
+                    <property name="sizing">fixed</property>
+                    <property name="min-width">195</property>
+                    <property name="title">Shortcut</property>
+                    <child>
+                      <object class="GtkCellRendererText" id="shortcut_cell">
+                        <property name="xalign">1.0</property>
+                        <property name="xpad">8</property>
+                        <property name="ypad">6</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/libide/shortcuts/ide-shortcut-theme-load.c b/libide/shortcuts/ide-shortcut-theme-load.c
new file mode 100644
index 0000000..585355e
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme-load.c
@@ -0,0 +1,665 @@
+/* ide-shortcut-theme-load.c
+ *
+ * Copyright (C) 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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-theme"
+
+#include <string.h>
+
+#include "ide-shortcut-context.h"
+#include "ide-shortcut-theme.h"
+
+typedef enum
+{
+  LOAD_STATE_THEME = 1,
+  LOAD_STATE_CONTEXT,
+  LOAD_STATE_PROPERTY,
+  LOAD_STATE_SHORTCUT,
+  LOAD_STATE_SIGNAL,
+  LOAD_STATE_PARAM,
+  LOAD_STATE_ACTION,
+} LoadStateType;
+
+typedef struct _LoadStateFrame
+{
+  LoadStateType           type;
+
+  /* Owned references */
+  struct _LoadStateFrame *next;
+  IdeShortcutContext     *context;
+  gchar                  *accelerator;
+  gchar                  *signal;
+  GSList                 *params;
+
+  /* Weak references */
+  GObject                *object;
+  GParamSpec             *pspec;
+
+  guint                   translatable : 1;
+} LoadStateFrame;
+
+typedef struct
+{
+  IdeShortcutTheme *self;
+  LoadStateFrame   *stack;
+  GString          *text;
+  const gchar      *translation_domain;
+  guint             in_param : 1;
+  guint             in_property : 1;
+} LoadState;
+
+static LoadStateFrame *
+load_state_frame_new (LoadStateType type)
+{
+  LoadStateFrame *frm;
+
+  frm = g_slice_new0 (LoadStateFrame);
+  frm->type = type;
+
+  return frm;
+}
+
+static void
+load_state_frame_free (LoadStateFrame *frm)
+{
+  g_clear_object (&frm->context);
+  g_clear_pointer (&frm->accelerator, g_free);
+  g_clear_pointer (&frm->signal, g_free);
+
+  g_slist_free_full (frm->params, g_free);
+  frm->params = NULL;
+
+  g_slice_free (LoadStateFrame, frm);
+}
+
+static void
+load_state_push (LoadState      *state,
+                 LoadStateFrame *frm)
+{
+  g_assert (state != NULL);
+  g_assert (frm != NULL);
+  g_assert (frm->next == NULL);
+
+  frm->next = state->stack;
+  state->stack = frm;
+}
+
+static gboolean
+load_state_check_type (LoadState      *state,
+                       LoadStateType   type,
+                       GError        **error)
+{
+  if (state->stack != NULL)
+    {
+      if (state->stack->type == type)
+        return TRUE;
+    }
+
+  g_set_error (error,
+               G_IO_ERROR,
+               G_IO_ERROR_FAILED,
+               "Unexpected stack when unwinding elements");
+
+  return FALSE;
+}
+
+static void
+load_state_pop (LoadState *state)
+{
+  LoadStateFrame *frm = state->stack;
+
+  if (frm != NULL)
+    {
+      state->stack = frm->next;
+      load_state_frame_free (frm);
+    }
+}
+
+static void
+load_state_add_action (LoadState   *state,
+                       const gchar *action)
+{
+  IdeShortcutContext *context = NULL;
+  const gchar *accel = NULL;
+
+  for (LoadStateFrame *iter = state->stack; iter != NULL; iter = iter->next)
+    {
+      if (iter->type == LOAD_STATE_SHORTCUT)
+        accel = iter->accelerator;
+      else if (iter->type == LOAD_STATE_CONTEXT)
+        context = iter->context;
+
+      if (accel && context)
+        break;
+    }
+
+  if (accel && context)
+    ide_shortcut_context_add_action (context, accel, action);
+}
+
+static void
+load_state_commit_param (LoadState *state)
+{
+  gchar *text;
+
+  g_assert (state->stack != NULL);
+  g_assert (state->stack->type == LOAD_STATE_SIGNAL);
+  g_assert (state->text != NULL);
+
+  text = g_string_free (state->text, FALSE);
+  state->text = NULL;
+  state->stack->params = g_slist_append (state->stack->params, text);
+}
+
+static void
+load_state_commit_property (LoadState  *state,
+                            GError    **error)
+{
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (state->stack != NULL);
+  g_assert (state->stack->type == LOAD_STATE_PROPERTY);
+  g_assert (state->stack->pspec != NULL);
+  g_assert (state->text != NULL);
+
+  /* XXX: Note this isn't super safe, since we are passing a NULL
+   *      GtkBuilder, but it does work for the cases we need to support.
+   *      But there is the chance for a NULL dereference that we should
+   *      probably protect against.
+   */
+  if (gtk_builder_value_from_string_type (NULL,
+                                          G_PARAM_SPEC_VALUE_TYPE (state->stack->pspec),
+                                          state->text->str,
+                                          &value,
+                                          error))
+    g_object_set_property (state->stack->object,
+                           state->stack->pspec->name,
+                           &value);
+
+  g_string_free (state->text, FALSE);
+  state->text = NULL;
+}
+
+static void
+parse_into_value (const gchar *str,
+                  GValue      *value)
+{
+  g_autofree gchar *lower = NULL;
+
+  /*
+   * We don't know the type at this point, so we rely on various
+   * GValueTransform to convert types at runtime upon signal emission. It adds
+   * some runtime overhead but allows more flexibility in where we emit
+   * signals from shortcuts.
+   */
+
+  if (!str || !*str)
+    {
+      g_value_init (value, G_TYPE_STRING);
+      return;
+    }
+
+  if (g_ascii_isdigit (*str) || *str == '-' || *str == '+')
+    {
+      if (strchr (str, '.') != NULL)
+        {
+          g_value_init (value, G_TYPE_DOUBLE);
+          g_value_set_double (value, g_ascii_strtod (str, NULL));
+        }
+      else
+        {
+          gint64 v = g_ascii_strtoll (str, NULL, 10);
+
+          if (ABS (v) <= G_MAXINT)
+            {
+              g_value_init (value, G_TYPE_INT);
+              g_value_set_int (value, v);
+            }
+          else
+            {
+              g_value_init (value, G_TYPE_INT64);
+              g_value_set_int64 (value, v);
+            }
+        }
+
+      return;
+    }
+
+  lower = g_utf8_strdown (str, -1);
+
+  if (g_str_equal (lower, "false"))
+    {
+      g_value_init (value, G_TYPE_BOOLEAN);
+      return;
+    }
+
+  if (g_str_equal (lower, "true"))
+    {
+      g_value_init (value, G_TYPE_BOOLEAN);
+      g_value_set_boolean (value, TRUE);
+      return;
+    }
+
+  g_value_init (value, G_TYPE_STRING);
+  g_value_set_string (value, str);
+}
+
+static void
+load_state_add_signal (LoadState *state)
+{
+  LoadStateFrame *signal;
+  LoadStateFrame *shortcut;
+  LoadStateFrame *context;
+  GArray *values;
+
+  g_assert (state->stack != NULL);
+  g_assert (state->stack->type == LOAD_STATE_SIGNAL);
+  g_assert (state->stack->next != NULL);
+  g_assert (state->stack->next->type == LOAD_STATE_SHORTCUT);
+  g_assert (state->stack->next->accelerator != NULL);
+  g_assert (state->stack->next->next->type == LOAD_STATE_CONTEXT);
+  g_assert (state->stack->next->next->context != NULL);
+
+  signal = state->stack;
+  shortcut = signal->next;
+  context = shortcut->next;
+
+  g_assert (signal->type == LOAD_STATE_SIGNAL);
+  g_assert (shortcut->type == LOAD_STATE_SHORTCUT);
+  g_assert (context->type == LOAD_STATE_CONTEXT);
+
+  values = g_array_sized_new (FALSE, FALSE, sizeof (GValue), g_slist_length (signal->params));
+
+  for (const GSList *iter = signal->params; iter != NULL; iter = iter->next)
+    {
+      const gchar *str = iter->data;
+      GValue value = G_VALUE_INIT;
+
+      parse_into_value (str, &value);
+
+      g_array_append_val (values, value);
+    }
+
+#if 0
+  g_print ("Adding signal %s to %s via %s\n",
+           signal->signal,
+           ide_shortcut_context_get_name (context->context),
+           shortcut->accelerator);
+#endif
+
+  ide_shortcut_context_add_signalv (context->context,
+                                    shortcut->accelerator,
+                                    signal->signal,
+                                    values);
+}
+
+static void
+theme_start_element (GMarkupParseContext  *context,
+                     const gchar          *element_name,
+                     const gchar         **attr_names,
+                     const gchar         **attr_values,
+                     gpointer              user_data,
+                     GError              **error)
+{
+  LoadState *state = user_data;
+
+  g_assert (state != NULL);
+  g_assert (IDE_IS_SHORTCUT_THEME (state->self));
+  g_assert (context != NULL);
+  g_assert (element_name != NULL);
+
+  if (g_strcmp0 (element_name, "theme") == 0)
+    {
+      const gchar *domain = NULL;
+
+      if (state->stack != NULL)
+        {
+          g_set_error (error,
+                       G_IO_ERROR,
+                       G_IO_ERROR_INVALID_DATA,
+                       "Got theme element in location other than root");
+          return;
+        }
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, 
"translation-domain", &domain,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      if (domain != NULL)
+        state->translation_domain = g_intern_string (domain);
+
+      load_state_push (state, load_state_frame_new (LOAD_STATE_THEME));
+    }
+  else if (g_strcmp0 (element_name, "property") == 0)
+    {
+      LoadStateFrame *frm;
+      const gchar *translatable = NULL;
+      const gchar *name = NULL;
+      GParamSpec *pspec;
+      GObject *obj = NULL;
+
+      if (!load_state_check_type (state, LOAD_STATE_CONTEXT, NULL) &&
+          !load_state_check_type (state, LOAD_STATE_THEME, NULL))
+        {
+          g_set_error (error,
+                       G_IO_ERROR,
+                       G_IO_ERROR_INVALID_DATA,
+                       "property only valid in theme or context");
+          return;
+        }
+
+      if (state->stack->type == LOAD_STATE_CONTEXT)
+        obj = G_OBJECT (state->stack->context);
+      else if (state->stack->type == LOAD_STATE_THEME)
+        obj = G_OBJECT (state->self);
+      else { g_assert_not_reached (); }
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "translatable", 
&translatable,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (obj), name);
+
+      if (pspec == NULL)
+        {
+          g_set_error (error,
+                       G_MARKUP_ERROR,
+                       G_MARKUP_ERROR_INVALID_CONTENT,
+                       "Failed to locate “%s” property",
+                       name);
+          return;
+        }
+
+      frm = load_state_frame_new (LOAD_STATE_PROPERTY);
+      frm->pspec = pspec;
+      frm->object = obj;
+      frm->translatable = translatable && (*translatable == 'y' || *translatable == 'Y');
+
+      load_state_push (state, frm);
+
+      state->in_property = TRUE;
+    }
+  else if (g_strcmp0 (element_name, "context") == 0)
+    {
+      LoadStateFrame *frm;
+      const gchar *name = NULL;
+
+      if (!load_state_check_type (state, LOAD_STATE_THEME, error))
+        return;
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      frm = load_state_frame_new (LOAD_STATE_CONTEXT);
+      frm->context = ide_shortcut_context_new (name);
+
+      load_state_push (state, frm);
+    }
+  else if (g_strcmp0 (element_name, "shortcut") == 0)
+    {
+      LoadStateFrame *frm;
+      const gchar *accelerator = NULL;
+      const gchar *action = NULL;
+      const gchar *signal = NULL;
+
+      if (!load_state_check_type (state, LOAD_STATE_CONTEXT, error))
+        return;
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING, "accelerator", &accelerator,
+                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "action", 
&action,
+                                        G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "signal", 
&signal,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      frm = load_state_frame_new (LOAD_STATE_SHORTCUT);
+      frm->accelerator = g_strdup (accelerator);
+      load_state_push (state, frm);
+
+      if (action != NULL)
+        load_state_add_action (state, action);
+
+      if (signal != NULL)
+        {
+          frm = load_state_frame_new (LOAD_STATE_SIGNAL);
+          frm->signal = g_strdup (signal);
+          load_state_push (state, frm);
+          load_state_add_signal (state);
+          load_state_pop (state);
+        }
+    }
+  else if (g_strcmp0 (element_name, "signal") == 0)
+    {
+      LoadStateFrame *frm;
+      const gchar *name = NULL;
+
+      if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error))
+        return;
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      frm = load_state_frame_new (LOAD_STATE_SIGNAL);
+      frm->signal = g_strdup (name);
+
+      load_state_push (state, frm);
+    }
+  else if (g_strcmp0 (element_name, "param") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error))
+        return;
+
+      state->in_param = TRUE;
+    }
+  else if (g_strcmp0 (element_name, "action") == 0)
+    {
+      const gchar *name = NULL;
+
+      if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error))
+        return;
+
+      if (!g_markup_collect_attributes (element_name, attr_names, attr_values, error,
+                                        G_MARKUP_COLLECT_STRING, "name", &name,
+                                        G_MARKUP_COLLECT_INVALID))
+        return;
+
+      load_state_add_action (state, name);
+    }
+}
+
+static void
+theme_end_element (GMarkupParseContext  *context,
+                   const gchar          *element_name,
+                   gpointer              user_data,
+                   GError              **error)
+{
+  LoadState *state = user_data;
+
+  g_assert (context != NULL);
+  g_assert (element_name != NULL);
+
+  if (g_strcmp0 (element_name, "theme") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_THEME, error))
+        return;
+    }
+  else if (g_strcmp0 (element_name, "property") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_PROPERTY, error))
+        return;
+
+      if (state->text)
+        load_state_commit_property (state, error);
+
+      state->in_property = FALSE;
+    }
+  else if (g_strcmp0 (element_name, "context") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_CONTEXT, error))
+        return;
+
+      ide_shortcut_theme_add_context (state->self, state->stack->context);
+    }
+  else if (g_strcmp0 (element_name, "shortcut") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_SHORTCUT, error))
+        return;
+    }
+  else if (g_strcmp0 (element_name, "signal") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error))
+        return;
+
+      load_state_add_signal (state);
+    }
+  else if (g_strcmp0 (element_name, "param") == 0)
+    {
+      if (!load_state_check_type (state, LOAD_STATE_SIGNAL, error))
+        return;
+
+      g_assert (state->in_param);
+
+      if (state->text)
+        load_state_commit_param (state);
+
+      state->in_param = FALSE;
+
+      return;
+    }
+  else if (g_strcmp0 (element_name, "action") == 0)
+    {
+      load_state_check_type (state, LOAD_STATE_SHORTCUT, error);
+      return;
+    }
+  else
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "Unexpected close element %s",
+                   element_name);
+      return;
+    }
+
+  load_state_pop (state);
+}
+
+static void
+theme_text (GMarkupParseContext  *context,
+            const gchar          *text,
+            gsize                 text_len,
+            gpointer              user_data,
+            GError              **error)
+{
+  LoadState *state = user_data;
+
+  g_assert (context != NULL);
+  g_assert (text != NULL);
+  g_assert (state != NULL);
+
+  if (state->in_param || state->in_property)
+    {
+      if ((state->in_param && !load_state_check_type (state, LOAD_STATE_SIGNAL, error)) ||
+          (state->in_property && !load_state_check_type (state, LOAD_STATE_PROPERTY, error)))
+        return;
+
+      if (state->text == NULL)
+        state->text = g_string_new (NULL);
+
+      g_string_append_len (state->text, text, text_len);
+    }
+}
+
+static const GMarkupParser theme_parser = {
+  .start_element = theme_start_element,
+  .end_element = theme_end_element,
+  .text = theme_text,
+};
+
+gboolean
+ide_shortcut_theme_load_from_data (IdeShortcutTheme  *self,
+                                   const gchar       *data,
+                                   gssize             len,
+                                   GError           **error)
+{
+  g_autoptr(GMarkupParseContext) context = NULL;
+  LoadState state = { 0 };
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (data != NULL, FALSE);
+
+  state.self = self;
+
+  context = g_markup_parse_context_new (&theme_parser, 0, &state, NULL);
+  ret = g_markup_parse_context_parse (context, data, len, error);
+
+  while (state.stack != NULL)
+    {
+      LoadStateFrame *frm = state.stack;
+      state.stack = frm->next;
+      load_state_frame_free (frm);
+    }
+
+  if (state.text)
+    g_string_free (state.text, TRUE);
+
+  return ret;
+}
+
+gboolean
+ide_shortcut_theme_load_from_file (IdeShortcutTheme  *self,
+                                   GFile             *file,
+                                   GCancellable      *cancellable,
+                                   GError           **error)
+{
+  g_autofree gchar *contents = NULL;
+  gsize len = 0;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (!g_file_load_contents (file, cancellable, &contents, &len, NULL, error))
+    return FALSE;
+
+  return ide_shortcut_theme_load_from_data (self, contents, len, error);
+}
+
+gboolean
+ide_shortcut_theme_load_from_path (IdeShortcutTheme  *self,
+                                   const gchar       *path,
+                                   GCancellable      *cancellable,
+                                   GError           **error)
+{
+  g_autoptr(GFile) file = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  file = g_file_new_for_path (path);
+
+  return ide_shortcut_theme_load_from_file (self, file, cancellable, error);
+}
diff --git a/libide/shortcuts/ide-shortcut-theme-save.c b/libide/shortcuts/ide-shortcut-theme-save.c
new file mode 100644
index 0000000..b89be0b
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme-save.c
@@ -0,0 +1,191 @@
+/* ide-shortcut-theme-save.c
+ *
+ * Copyright (C) 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 2 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 "ide-shortcut-theme.h"
+#include "ide-shortcut-private.h"
+
+gboolean
+ide_shortcut_theme_save_to_stream (IdeShortcutTheme  *self,
+                                   GOutputStream     *stream,
+                                   GCancellable      *cancellable,
+                                   GError           **error)
+{
+  g_autoptr(GString) str = NULL;
+  IdeShortcutContext *context;
+  GHashTable *contexts;
+  GHashTableIter iter;
+  const gchar *name;
+  const gchar *title;
+  const gchar *subtitle;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (G_IS_OUTPUT_STREAM (stream), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  contexts = _ide_shortcut_theme_get_contexts (self);
+
+  str = g_string_new ("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
+
+  g_string_append (str, "<theme>\n");
+
+  name = ide_shortcut_theme_get_name (self);
+  title = ide_shortcut_theme_get_title (self);
+  subtitle = ide_shortcut_theme_get_subtitle (self);
+
+  g_string_append_printf (str, "  <property name=\"name\">%s</property>\n", name ? name : "");
+  g_string_append_printf (str, "  <property name=\"title\" translatable=\"yes\">%s</property>\n", title ? 
title : "");
+  g_string_append_printf (str, "  <property name=\"subtitle\" translatable=\"yes\">%s</property>\n", 
subtitle ? subtitle : "");
+
+  g_hash_table_iter_init (&iter, contexts);
+
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&context))
+    {
+      IdeShortcutChordTable *table;
+      IdeShortcutChordTableIter citer;
+      gboolean use_binding_sets = FALSE;
+      const IdeShortcutChord *chord = NULL;
+      Shortcut *shortcut = NULL;
+
+      table = _ide_shortcut_context_get_table (context);
+      name = ide_shortcut_context_get_name (context);
+      g_object_get (context, "use-binding-sets", &use_binding_sets, NULL);
+
+      g_string_append_printf (str, "  <context name=\"%s\">\n", name);
+
+      if (!use_binding_sets)
+        g_string_append (str, "    <property name=\"use-binding-sets\">false</property>\n");
+
+      _ide_shortcut_chord_table_iter_init (&citer, table);
+
+      while (_ide_shortcut_chord_table_iter_next (&citer, &chord, (gpointer *)&shortcut))
+        {
+          g_autofree gchar *accel = ide_shortcut_chord_to_string (chord);
+
+          g_string_append_printf (str, "    <shortcut accelerator=\"%s\">\n", accel);
+
+          for (; shortcut != NULL; shortcut = shortcut->next)
+            {
+              if (shortcut->type == SHORTCUT_ACTION)
+                {
+                  if (shortcut->action.param == NULL)
+                    {
+                      g_string_append_printf (str, "      <action name=\"%s.%s\"/>\n",
+                                              shortcut->action.prefix, shortcut->action.name);
+                    }
+                  else
+                    {
+                      g_autofree gchar *fmt = g_variant_print (shortcut->action.param, FALSE);
+                      g_string_append_printf (str, "      <action name=\"%s.%s::%s\"/>\n",
+                                              shortcut->action.prefix, shortcut->action.name, fmt);
+                    }
+                }
+              else if (shortcut->type == SHORTCUT_SIGNAL)
+                {
+                  if (shortcut->signal.detail)
+                    g_string_append_printf (str, "      <signal name=\"%s::%s\"",
+                                            shortcut->signal.name,
+                                            g_quark_to_string (shortcut->signal.detail));
+                  else
+                    g_string_append_printf (str, "      <signal name=\"%s\"",
+                                            shortcut->signal.name);
+
+                  if (shortcut->signal.params == NULL || shortcut->signal.params->len == 0)
+                    {
+                      g_string_append (str, "/>\n");
+                      continue;
+                    }
+
+                  g_string_append (str, ">\n");
+
+                  for (guint j = 0; j < shortcut->signal.params->len; j++)
+                    {
+                      GValue *value = &g_array_index (shortcut->signal.params, GValue, j);
+
+                      if (G_VALUE_HOLDS_STRING (value))
+                        {
+                          g_autofree gchar *escape = g_markup_escape_text (g_value_get_string (value), -1);
+
+                          g_string_append_printf (str, "        <param>\"%s\"</param>\n", escape);
+                        }
+                      else
+                        {
+                          g_auto(GValue) translated = G_VALUE_INIT;
+
+                          g_value_init (&translated, G_TYPE_STRING);
+                          g_value_transform (value, &translated);
+                          g_string_append_printf (str, "        <param>%s</param>\n", g_value_get_string 
(&translated));
+                        }
+                    }
+
+                  g_string_append (str, "      </signal>\n");
+
+                }
+            }
+
+          g_string_append (str, "    </shortcut>\n");
+        }
+
+      g_string_append (str, "  </context>\n");
+    }
+
+  g_string_append (str, "</theme>\n");
+
+  return g_output_stream_write_all (stream, str->str, str->len, NULL, cancellable, error);
+}
+
+gboolean
+ide_shortcut_theme_save_to_file (IdeShortcutTheme  *self,
+                                 GFile             *file,
+                                 GCancellable      *cancellable,
+                                 GError           **error)
+{
+  g_autoptr(GFileOutputStream) stream = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  stream = g_file_replace (file,
+                           NULL,
+                           FALSE,
+                           G_FILE_CREATE_REPLACE_DESTINATION,
+                           cancellable,
+                           error);
+
+  if (stream == NULL)
+    return FALSE;
+
+  return ide_shortcut_theme_save_to_stream (self, G_OUTPUT_STREAM (stream), cancellable, error);
+}
+
+gboolean
+ide_shortcut_theme_save_to_path (IdeShortcutTheme  *self,
+                                 const gchar       *path,
+                                 GCancellable      *cancellable,
+                                 GError           **error)
+{
+  g_autoptr(GFile) file = NULL;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  file = g_file_new_for_path (path);
+
+  return ide_shortcut_theme_save_to_file (self, file, cancellable, error);
+}
diff --git a/libide/shortcuts/ide-shortcut-theme.c b/libide/shortcuts/ide-shortcut-theme.c
new file mode 100644
index 0000000..d26d5a5
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.c
@@ -0,0 +1,387 @@
+/* ide-shortcut-theme.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 2 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-theme"
+
+#include "ide-shortcut-private.h"
+#include "ide-shortcut-theme.h"
+
+typedef struct
+{
+  gchar      *name;
+  gchar      *title;
+  gchar      *subtitle;
+  GHashTable *contexts;
+  GHashTable *action_to_chord;
+  GHashTable *command_to_chord;
+} IdeShortcutThemePrivate;
+
+enum {
+  PROP_0,
+  PROP_NAME,
+  PROP_SUBTITLE,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutTheme, ide_shortcut_theme, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_theme_finalize (GObject *object)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->subtitle, g_free);
+  g_clear_pointer (&priv->contexts, g_hash_table_unref);
+  g_clear_pointer (&priv->action_to_chord, g_hash_table_unref);
+  g_clear_pointer (&priv->command_to_chord, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_shortcut_theme_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcut_theme_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, ide_shortcut_theme_get_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_shortcut_theme_get_title (self));
+      break;
+
+    case PROP_SUBTITLE:
+      g_value_set_string (value, ide_shortcut_theme_get_subtitle (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutTheme *self = (IdeShortcutTheme *)object;
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      priv->title = g_value_dup_string (value);
+      break;
+
+    case PROP_SUBTITLE:
+      priv->subtitle = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_theme_class_init (IdeShortcutThemeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_shortcut_theme_finalize;
+  object_class->get_property = ide_shortcut_theme_get_property;
+  object_class->set_property = ide_shortcut_theme_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the theme",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the theme as used for UI elements",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "The subtitle of the theme as used for UI elements",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_theme_init (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  priv->contexts = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+  priv->action_to_chord = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+                                                 (GDestroyNotify)ide_shortcut_chord_free);
+  priv->command_to_chord = g_hash_table_new_full (g_str_hash, g_str_equal, NULL,
+                                                  (GDestroyNotify)ide_shortcut_chord_free);
+}
+
+const gchar *
+ide_shortcut_theme_get_name (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+
+  return priv->name;
+}
+
+/**
+ * ide_shortcut_theme_find_context_by_name:
+ * @self: An #IdeShortcutContext
+ * @name: The name of the context
+ *
+ * Gets the context named @name. If the context does not exist, it will
+ * be created.
+ *
+ * Returns: (not nullable) (transfer none): An #IdeShortcutContext
+ */
+IdeShortcutContext *
+ide_shortcut_theme_find_context_by_name (IdeShortcutTheme *self,
+                                         const gchar      *name)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+  IdeShortcutContext *ret;
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  if (NULL == (ret = g_hash_table_lookup (priv->contexts, name)))
+    {
+      ret = ide_shortcut_context_new (name);
+      g_hash_table_insert (priv->contexts, g_strdup (name), ret);
+    }
+
+  return ret;
+}
+
+static IdeShortcutContext *
+ide_shortcut_theme_find_default_context_by_type (IdeShortcutTheme *self,
+                                                 GType             type)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, GTK_TYPE_WIDGET), NULL);
+
+  return ide_shortcut_theme_find_context_by_name (self, g_type_name (type));
+}
+
+/**
+ * ide_shortcut_theme_find_default_context:
+ *
+ * Finds the default context in the theme for @widget.
+ *
+ * Returns: (nullable) (transfer none): An #IdeShortcutContext or %NULL.
+ */
+IdeShortcutContext *
+ide_shortcut_theme_find_default_context (IdeShortcutTheme *self,
+                                         GtkWidget        *widget)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  return ide_shortcut_theme_find_default_context_by_type (self, G_OBJECT_TYPE (widget));
+}
+
+void
+ide_shortcut_theme_add_context (IdeShortcutTheme   *self,
+                                IdeShortcutContext *context)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+  const gchar *name;
+
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (IDE_IS_SHORTCUT_CONTEXT (context));
+
+  name = ide_shortcut_context_get_name (context);
+
+  g_return_if_fail (name != NULL);
+
+  g_hash_table_insert (priv->contexts, g_strdup (name), g_object_ref (context));
+}
+
+IdeShortcutTheme *
+ide_shortcut_theme_new (const gchar *name)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_THEME,
+                       "name", name,
+                       NULL);
+}
+
+const gchar *
+ide_shortcut_theme_get_title (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+
+  return priv->title;
+}
+
+const gchar *
+ide_shortcut_theme_get_subtitle (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+
+  return priv->subtitle;
+}
+
+GHashTable *
+_ide_shortcut_theme_get_contexts (IdeShortcutTheme *self)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+
+  return priv->contexts;
+}
+
+const IdeShortcutChord *
+ide_shortcut_theme_get_chord_for_action (IdeShortcutTheme *self,
+                                         const gchar      *detailed_action_name)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (detailed_action_name != NULL, NULL);
+
+  return g_hash_table_lookup (priv->action_to_chord, detailed_action_name);
+}
+
+void
+ide_shortcut_theme_set_chord_for_action (IdeShortcutTheme       *self,
+                                         const gchar            *detailed_action_name,
+                                         const IdeShortcutChord *chord)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (detailed_action_name != NULL);
+
+  detailed_action_name = g_intern_string (detailed_action_name);
+
+  if (chord == NULL)
+    g_hash_table_remove (priv->action_to_chord, detailed_action_name);
+  else
+    g_hash_table_insert (priv->action_to_chord,
+                         (gchar *)detailed_action_name,
+                         ide_shortcut_chord_copy (chord));
+}
+
+void
+ide_shortcut_theme_set_accel_for_action (IdeShortcutTheme *self,
+                                         const gchar      *detailed_action_name,
+                                         const gchar      *accel)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (detailed_action_name != NULL);
+
+  if (accel == NULL)
+    {
+      ide_shortcut_theme_set_chord_for_action (self, detailed_action_name, NULL);
+    }
+  else
+    {
+      g_autoptr(IdeShortcutChord) chord = NULL;
+
+      chord = ide_shortcut_chord_new_from_string (accel);
+      ide_shortcut_theme_set_chord_for_action (self, detailed_action_name, chord);
+    }
+}
+
+const IdeShortcutChord *
+ide_shortcut_theme_get_chord_for_command (IdeShortcutTheme *self,
+                                          const gchar      *detailed_command_name)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SHORTCUT_THEME (self), NULL);
+  g_return_val_if_fail (detailed_command_name != NULL, NULL);
+
+  return g_hash_table_lookup (priv->command_to_chord, detailed_command_name);
+}
+
+void
+ide_shortcut_theme_set_chord_for_command (IdeShortcutTheme       *self,
+                                          const gchar            *detailed_command_name,
+                                          const IdeShortcutChord *chord)
+{
+  IdeShortcutThemePrivate *priv = ide_shortcut_theme_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (detailed_command_name != NULL);
+
+  detailed_command_name = g_intern_string (detailed_command_name);
+
+  if (chord == NULL)
+    g_hash_table_remove (priv->command_to_chord, detailed_command_name);
+  else
+    g_hash_table_insert (priv->command_to_chord,
+                         (gchar *)detailed_command_name,
+                         ide_shortcut_chord_copy (chord));
+}
+
+void
+ide_shortcut_theme_set_accel_for_command (IdeShortcutTheme *self,
+                                          const gchar      *detailed_command_name,
+                                          const gchar      *accel)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_THEME (self));
+  g_return_if_fail (detailed_command_name != NULL);
+
+  if (accel == NULL)
+    {
+      ide_shortcut_theme_set_chord_for_command (self, detailed_command_name, NULL);
+    }
+  else
+    {
+      g_autoptr(IdeShortcutChord) chord = NULL;
+
+      chord = ide_shortcut_chord_new_from_string (accel);
+      ide_shortcut_theme_set_chord_for_command (self, detailed_command_name, chord);
+    }
+}
diff --git a/libide/shortcuts/ide-shortcut-theme.h b/libide/shortcuts/ide-shortcut-theme.h
new file mode 100644
index 0000000..5fd8b82
--- /dev/null
+++ b/libide/shortcuts/ide-shortcut-theme.h
@@ -0,0 +1,100 @@
+/* ide-shortcut-theme.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 2 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 IDE_SHORTCUT_THEME_H
+#define IDE_SHORTCUT_THEME_H
+
+#include <gtk/gtk.h>
+
+#include "ide-shortcut-chord.h"
+#include "ide-shortcut-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_THEME (ide_shortcut_theme_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeShortcutTheme, ide_shortcut_theme, IDE, SHORTCUT_THEME, GObject)
+
+struct _IdeShortcutThemeClass
+{
+  GObjectClass parent_class;
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+IdeShortcutTheme       *ide_shortcut_theme_new                   (const gchar             *name);
+const gchar            *ide_shortcut_theme_get_name              (IdeShortcutTheme        *self);
+const gchar            *ide_shortcut_theme_get_title             (IdeShortcutTheme        *self);
+const gchar            *ide_shortcut_theme_get_subtitle          (IdeShortcutTheme        *self);
+IdeShortcutContext     *ide_shortcut_theme_find_default_context  (IdeShortcutTheme        *self,
+                                                                  GtkWidget               *widget);
+IdeShortcutContext     *ide_shortcut_theme_find_context_by_name  (IdeShortcutTheme        *self,
+                                                                  const gchar             *name);
+void                    ide_shortcut_theme_add_context           (IdeShortcutTheme        *self,
+                                                                  IdeShortcutContext      *context);
+void                    ide_shortcut_theme_set_chord_for_action  (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_action_name,
+                                                                  const IdeShortcutChord  *chord);
+const IdeShortcutChord *ide_shortcut_theme_get_chord_for_action  (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_action_name);
+void                    ide_shortcut_theme_set_accel_for_action  (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_action_name,
+                                                                  const gchar             *accel);
+void                    ide_shortcut_theme_set_chord_for_command (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_command_name,
+                                                                  const IdeShortcutChord  *chord);
+const IdeShortcutChord *ide_shortcut_theme_get_chord_for_command (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_command_name);
+void                    ide_shortcut_theme_set_accel_for_command (IdeShortcutTheme        *self,
+                                                                  const gchar             
*detailed_command_name,
+                                                                  const gchar             *accel);
+gboolean                ide_shortcut_theme_load_from_data        (IdeShortcutTheme        *self,
+                                                                  const gchar             *data,
+                                                                  gssize                   len,
+                                                                  GError                 **error);
+gboolean                ide_shortcut_theme_load_from_file        (IdeShortcutTheme        *self,
+                                                                  GFile                   *file,
+                                                                  GCancellable            *cancellable,
+                                                                  GError                 **error);
+gboolean                ide_shortcut_theme_load_from_path        (IdeShortcutTheme        *self,
+                                                                  const gchar             *path,
+                                                                  GCancellable            *cancellable,
+                                                                  GError                 **error);
+gboolean                ide_shortcut_theme_save_to_file          (IdeShortcutTheme        *self,
+                                                                  GFile                   *file,
+                                                                  GCancellable            *cancellable,
+                                                                  GError                 **error);
+gboolean                ide_shortcut_theme_save_to_stream        (IdeShortcutTheme        *self,
+                                                                  GOutputStream           *stream,
+                                                                  GCancellable            *cancellable,
+                                                                  GError                 **error);
+gboolean                ide_shortcut_theme_save_to_path          (IdeShortcutTheme        *self,
+                                                                  const gchar             *path,
+                                                                  GCancellable            *cancellable,
+                                                                  GError                 **error);
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUT_THEME_H */
diff --git a/libide/shortcuts/ide-shortcuts-group.c b/libide/shortcuts/ide-shortcuts-group.c
new file mode 100644
index 0000000..039722b
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-group.c
@@ -0,0 +1,381 @@
+/* ide-shortcuts-group.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcuts-group.h"
+#include "ide-shortcuts-shortcut.h"
+
+/**
+ * SECTION:ide-shortcuts-group
+ * @Title: IdeShortcutsGroup
+ * @Short_description: Represents a group of shortcuts in a IdeShortcutsWindow
+ *
+ * A IdeShortcutsGroup represents a group of related keyboard shortcuts
+ * or gestures. The group has a title. It may optionally be associated with
+ * a view of the application, which can be used to show only relevant shortcuts
+ * depending on the application context.
+ *
+ * This widget is only meant to be used with #IdeShortcutsWindow.
+ */
+
+struct _IdeShortcutsGroup
+{
+  GtkBox    parent_instance;
+
+  GtkLabel *title;
+  gchar    *view;
+  guint     height;
+
+  GtkSizeGroup *accel_size_group;
+  GtkSizeGroup *title_size_group;
+};
+
+struct _IdeShortcutsGroupClass
+{
+  GtkBoxClass parent_class;
+};
+
+G_DEFINE_TYPE (IdeShortcutsGroup, ide_shortcuts_group, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_VIEW,
+  PROP_ACCEL_SIZE_GROUP,
+  PROP_TITLE_SIZE_GROUP,
+  PROP_HEIGHT,
+  LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP];
+
+static void
+ide_shortcuts_group_apply_accel_size_group (IdeShortcutsGroup *group,
+                                            GtkWidget         *child)
+{
+  if (IDE_IS_SHORTCUTS_SHORTCUT (child))
+    g_object_set (child, "accel-size-group", group->accel_size_group, NULL);
+}
+
+static void
+ide_shortcuts_group_apply_title_size_group (IdeShortcutsGroup *group,
+                                            GtkWidget         *child)
+{
+  if (IDE_IS_SHORTCUTS_SHORTCUT (child))
+    g_object_set (child, "title-size-group", group->title_size_group, NULL);
+}
+
+static void
+ide_shortcuts_group_set_accel_size_group (IdeShortcutsGroup *group,
+                                          GtkSizeGroup      *size_group)
+{
+  GList *children, *l;
+
+  g_set_object (&group->accel_size_group, size_group);
+
+  children = gtk_container_get_children (GTK_CONTAINER (group));
+  for (l = children; l; l = l->next)
+    ide_shortcuts_group_apply_accel_size_group (group, GTK_WIDGET (l->data));
+  g_list_free (children);
+}
+
+static void
+ide_shortcuts_group_set_title_size_group (IdeShortcutsGroup *group,
+                                          GtkSizeGroup      *size_group)
+{
+  GList *children, *l;
+
+  g_set_object (&group->title_size_group, size_group);
+
+  children = gtk_container_get_children (GTK_CONTAINER (group));
+  for (l = children; l; l = l->next)
+    ide_shortcuts_group_apply_title_size_group (group, GTK_WIDGET (l->data));
+  g_list_free (children);
+}
+
+static guint
+ide_shortcuts_group_get_height (IdeShortcutsGroup *group)
+{
+  GList *children, *l;
+  guint height;
+
+  height = 1;
+
+  children = gtk_container_get_children (GTK_CONTAINER (group));
+  for (l = children; l; l = l->next)
+    {
+      GtkWidget *child = l->data;
+
+      if (!gtk_widget_get_visible (child))
+        continue;
+      else if (IDE_IS_SHORTCUTS_SHORTCUT (child))
+        height += 1;
+    }
+  g_list_free (children);
+
+  return height;
+}
+
+static void
+ide_shortcuts_group_add (GtkContainer *container,
+                         GtkWidget    *widget)
+{
+  if (IDE_IS_SHORTCUTS_SHORTCUT (widget))
+    {
+      GTK_CONTAINER_CLASS (ide_shortcuts_group_parent_class)->add (container, widget);
+      ide_shortcuts_group_apply_accel_size_group (IDE_SHORTCUTS_GROUP (container), widget);
+      ide_shortcuts_group_apply_title_size_group (IDE_SHORTCUTS_GROUP (container), widget);
+    }
+  else
+    g_warning ("Can't add children of type %s to %s",
+               G_OBJECT_TYPE_NAME (widget),
+               G_OBJECT_TYPE_NAME (container));
+}
+
+typedef struct {
+  GtkCallback callback;
+  gpointer data;
+  gboolean include_internal;
+} CallbackData;
+
+static void
+forall_cb (GtkWidget *widget, gpointer data)
+{
+  IdeShortcutsGroup *self;
+  CallbackData *cbdata = data;
+
+  self = IDE_SHORTCUTS_GROUP (gtk_widget_get_parent (widget));
+  if (cbdata->include_internal || widget != (GtkWidget*)self->title)
+    cbdata->callback (widget, cbdata->data);
+}
+
+static void
+ide_shortcuts_group_forall (GtkContainer *container,
+                            gboolean      include_internal,
+                            GtkCallback   callback,
+                            gpointer      callback_data)
+{
+  CallbackData cbdata;
+
+  cbdata.include_internal = include_internal;
+  cbdata.callback = callback;
+  cbdata.data = callback_data;
+
+  GTK_CONTAINER_CLASS (ide_shortcuts_group_parent_class)->forall (container, include_internal, forall_cb, 
&cbdata);
+}
+
+static void
+ide_shortcuts_group_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  IdeShortcutsGroup *self = IDE_SHORTCUTS_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    case PROP_VIEW:
+      g_value_set_string (value, self->view);
+      break;
+
+    case PROP_HEIGHT:
+      g_value_set_uint (value, ide_shortcuts_group_get_height (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_group_direction_changed (GtkWidget        *widget,
+                                       GtkTextDirection  previous_dir)
+{
+  GTK_WIDGET_CLASS (ide_shortcuts_group_parent_class)->direction_changed (widget, previous_dir);
+  g_object_notify (G_OBJECT (widget), "height");
+}
+
+static void
+ide_shortcuts_group_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeShortcutsGroup *self = IDE_SHORTCUTS_GROUP (object);
+
+  switch (prop_id)
+    {
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    case PROP_VIEW:
+      g_free (self->view);
+      self->view = g_value_dup_string (value);
+      break;
+
+    case PROP_ACCEL_SIZE_GROUP:
+      ide_shortcuts_group_set_accel_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value)));
+      break;
+
+    case PROP_TITLE_SIZE_GROUP:
+      ide_shortcuts_group_set_title_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_group_finalize (GObject *object)
+{
+  IdeShortcutsGroup *self = IDE_SHORTCUTS_GROUP (object);
+
+  g_free (self->view);
+  g_set_object (&self->accel_size_group, NULL);
+  g_set_object (&self->title_size_group, NULL);
+
+  G_OBJECT_CLASS (ide_shortcuts_group_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcuts_group_dispose (GObject *object)
+{
+  IdeShortcutsGroup *self = IDE_SHORTCUTS_GROUP (object);
+
+  /*
+   * Since we overload forall(), the inherited destroy() won't work as normal.
+   * Remove internal widgets ourself.
+   */
+  if (self->title)
+    {
+      gtk_widget_destroy (GTK_WIDGET (self->title));
+      self->title = NULL;
+    }
+
+  G_OBJECT_CLASS (ide_shortcuts_group_parent_class)->dispose (object);
+}
+
+static void
+ide_shortcuts_group_class_init (IdeShortcutsGroupClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = ide_shortcuts_group_finalize;
+  object_class->get_property = ide_shortcuts_group_get_property;
+  object_class->set_property = ide_shortcuts_group_set_property;
+  object_class->dispose = ide_shortcuts_group_dispose;
+
+  widget_class->direction_changed = ide_shortcuts_group_direction_changed;
+  container_class->add = ide_shortcuts_group_add;
+  container_class->forall = ide_shortcuts_group_forall;
+
+  /**
+   * IdeShortcutsGroup:title:
+   *
+   * The title for this group of shortcuts.
+   */
+  properties[PROP_TITLE] =
+    g_param_spec_string ("title", _("Title"), _("Title"),
+                         "",
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsGroup:view:
+   *
+   * An optional view that the shortcuts in this group are relevant for.
+   * The group will be hidden if the #IdeShortcutsWindow:view-name property
+   * does not match the view of this group.
+   *
+   * Set this to %NULL to make the group always visible.
+   */
+  properties[PROP_VIEW] =
+    g_param_spec_string ("view", _("View"), _("View"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsGroup:accel-size-group:
+   *
+   * The size group for the accelerator portion of shortcuts in this group.
+   *
+   * This is used internally by GTK+, and must not be modified by applications.
+   */
+  properties[PROP_ACCEL_SIZE_GROUP] =
+    g_param_spec_object ("accel-size-group",
+                         _("Accelerator Size Group"),
+                         _("Accelerator Size Group"),
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsGroup:title-size-group:
+   *
+   * The size group for the textual portion of shortcuts in this group.
+   *
+   * This is used internally by GTK+, and must not be modified by applications.
+   */
+  properties[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));
+
+  /**
+   * IdeShortcutsGroup:height:
+   *
+   * A rough measure for the number of lines in this group.
+   *
+   * This is used internally by GTK+, and is not useful for applications.
+   */
+  properties[PROP_HEIGHT] =
+    g_param_spec_uint ("height", _("Height"), _("Height"),
+                       0, G_MAXUINT, 1,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_shortcuts_group_init (IdeShortcutsGroup *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);
+  pango_attr_list_unref (attrs);
+
+  GTK_CONTAINER_CLASS (ide_shortcuts_group_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(self->title));
+}
diff --git a/libide/shortcuts/ide-shortcuts-group.h b/libide/shortcuts/ide-shortcuts-group.h
new file mode 100644
index 0000000..4a76d8a
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-group.h
@@ -0,0 +1,41 @@
+/* ide-shortcuts-groupprivate.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __IDE_SHORTCUTS_GROUP_H__
+#define __IDE_SHORTCUTS_GROUP_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_GROUP            (ide_shortcuts_group_get_type ())
+#define IDE_SHORTCUTS_GROUP(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_SHORTCUTS_GROUP, 
IdeShortcutsGroup))
+#define IDE_SHORTCUTS_GROUP_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_SHORTCUTS_GROUP, 
IdeShortcutsGroupClass))
+#define IDE_IS_SHORTCUTS_GROUP(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IDE_TYPE_SHORTCUTS_GROUP))
+#define IDE_IS_SHORTCUTS_GROUP_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_SHORTCUTS_GROUP))
+#define IDE_SHORTCUTS_GROUP_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_SHORTCUTS_GROUP, 
IdeShortcutsGroupClass))
+
+
+typedef struct _IdeShortcutsGroup         IdeShortcutsGroup;
+typedef struct _IdeShortcutsGroupClass    IdeShortcutsGroupClass;
+
+GType ide_shortcuts_group_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* __IDE_SHORTCUTS_GROUP_H__ */
diff --git a/libide/shortcuts/ide-shortcuts-section.c b/libide/shortcuts/ide-shortcuts-section.c
new file mode 100644
index 0000000..a6695f4
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-section.c
@@ -0,0 +1,817 @@
+/* ide-shortcuts-section.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcuts-group.h"
+#include "ide-shortcuts-section.h"
+
+/**
+ * SECTION:ide-shortcuts-section
+ * @Title: IdeShortcutsSection
+ * @Short_description: Represents an application mode in a IdeShortcutsWindow
+ *
+ * A IdeShortcutsSection collects all the keyboard shortcuts and gestures
+ * for a major application mode. If your application needs multiple sections,
+ * you should give each section a unique #IdeShortcutsSection:section-name and
+ * a #IdeShortcutsSection:title that can be shown in the section selector of
+ * the IdeShortcutsWindow.
+ *
+ * The #IdeShortcutsSection:max-height property can be used to influence how
+ * the groups in the section are distributed over pages and columns.
+ *
+ * This widget is only meant to be used with #IdeShortcutsWindow.
+ */
+
+struct _IdeShortcutsSection
+{
+  GtkBox            parent_instance;
+
+  gchar            *name;
+  gchar            *title;
+  gchar            *view_name;
+  guint             max_height;
+
+  GtkStack         *stack;
+  GtkStackSwitcher *switcher;
+  GtkWidget        *show_all;
+  GtkWidget        *footer;
+  GList            *groups;
+
+  gboolean          has_filtered_group;
+  gboolean          need_reflow;
+
+  GtkGesture       *pan_gesture;
+};
+
+struct _IdeShortcutsSectionClass
+{
+  GtkBoxClass parent_class;
+
+  gboolean (* change_current_page) (IdeShortcutsSection *self,
+                                    gint                 offset);
+
+};
+
+G_DEFINE_TYPE (IdeShortcutsSection, ide_shortcuts_section, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_TITLE,
+  PROP_SECTION_NAME,
+  PROP_VIEW_NAME,
+  PROP_MAX_HEIGHT,
+  LAST_PROP
+};
+
+enum {
+  CHANGE_CURRENT_PAGE,
+  LAST_SIGNAL
+};
+
+static GParamSpec *properties[LAST_PROP];
+static guint signals[LAST_SIGNAL];
+
+static void ide_shortcuts_section_set_view_name    (IdeShortcutsSection *self,
+                                                    const gchar         *view_name);
+static void ide_shortcuts_section_set_max_height   (IdeShortcutsSection *self,
+                                                    guint                max_height);
+static void ide_shortcuts_section_add_group        (IdeShortcutsSection *self,
+                                                    IdeShortcutsGroup   *group);
+
+static void ide_shortcuts_section_show_all         (IdeShortcutsSection *self);
+static void ide_shortcuts_section_filter_groups    (IdeShortcutsSection *self);
+static void ide_shortcuts_section_reflow_groups    (IdeShortcutsSection *self);
+static void ide_shortcuts_section_maybe_reflow     (IdeShortcutsSection *self);
+
+static gboolean ide_shortcuts_section_change_current_page (IdeShortcutsSection *self,
+                                                           gint                 offset);
+
+static void ide_shortcuts_section_pan_gesture_pan (GtkGesturePan       *gesture,
+                                                   GtkPanDirection      direction,
+                                                   gdouble              offset,
+                                                   IdeShortcutsSection *self);
+
+static void
+ide_shortcuts_section_add (GtkContainer *container,
+                           GtkWidget    *child)
+{
+  IdeShortcutsSection *self = IDE_SHORTCUTS_SECTION (container);
+
+  if (IDE_IS_SHORTCUTS_GROUP (child))
+    ide_shortcuts_section_add_group (self, IDE_SHORTCUTS_GROUP (child));
+  else
+    g_warning ("Can't add children of type %s to %s",
+               G_OBJECT_TYPE_NAME (child),
+               G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+ide_shortcuts_section_remove (GtkContainer *container,
+                              GtkWidget    *child)
+{
+  IdeShortcutsSection *self = (IdeShortcutsSection *)container;
+
+  if (IDE_IS_SHORTCUTS_GROUP (child) &&
+      gtk_widget_is_ancestor (child, GTK_WIDGET (container)))
+    {
+      self->groups = g_list_remove (self->groups, child);
+      gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (child)), child);
+    }
+  else
+    GTK_CONTAINER_CLASS (ide_shortcuts_section_parent_class)->remove (container, child);
+}
+
+static void
+ide_shortcuts_section_forall (GtkContainer *container,
+                              gboolean      include_internal,
+                              GtkCallback   callback,
+                              gpointer      callback_data)
+{
+  IdeShortcutsSection *self = (IdeShortcutsSection *)container;
+  GList *l;
+
+  if (include_internal)
+    {
+      GTK_CONTAINER_CLASS (ide_shortcuts_section_parent_class)->forall (container, include_internal, 
callback, callback_data);
+    }
+  else
+    {
+      for (l = self->groups; l; l = l->next)
+        {
+          GtkWidget *group = l->data;
+          callback (group, callback_data);
+        }
+    }
+}
+
+static void
+map_child (GtkWidget *child)
+{
+  if (gtk_widget_get_visible (child) &&
+      gtk_widget_get_child_visible (child) &&
+      !gtk_widget_get_mapped (child))
+    gtk_widget_map (child);
+}
+
+static void
+ide_shortcuts_section_map (GtkWidget *widget)
+{
+  IdeShortcutsSection *self = IDE_SHORTCUTS_SECTION (widget);
+
+  if (self->need_reflow)
+    ide_shortcuts_section_reflow_groups (self);
+
+  gtk_widget_set_mapped (widget, TRUE);
+
+  map_child (GTK_WIDGET (self->stack));
+  map_child (GTK_WIDGET (self->footer));
+}
+
+static void
+ide_shortcuts_section_unmap (GtkWidget *widget)
+{
+  IdeShortcutsSection *self = IDE_SHORTCUTS_SECTION (widget);
+
+  gtk_widget_set_mapped (widget, FALSE);
+
+  gtk_widget_unmap (GTK_WIDGET (self->footer));
+  gtk_widget_unmap (GTK_WIDGET (self->stack));
+}
+
+static void
+ide_shortcuts_section_destroy (GtkWidget *widget)
+{
+  IdeShortcutsSection *self = IDE_SHORTCUTS_SECTION (widget);
+
+  if (self->stack)
+    {
+      gtk_widget_destroy (GTK_WIDGET (self->stack));
+      self->stack = NULL;
+    }
+
+  if (self->footer)
+    {
+      gtk_widget_destroy (GTK_WIDGET (self->footer));
+      self->footer = NULL;
+    }
+
+  g_list_free (self->groups);
+  self->groups = NULL;
+
+  GTK_WIDGET_CLASS (ide_shortcuts_section_parent_class)->destroy (widget);
+}
+
+static void
+ide_shortcuts_section_finalize (GObject *object)
+{
+  IdeShortcutsSection *self = (IdeShortcutsSection *)object;
+
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->title, g_free);
+  g_clear_pointer (&self->view_name, g_free);
+  g_clear_object (&self->pan_gesture);
+
+  G_OBJECT_CLASS (ide_shortcuts_section_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcuts_section_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeShortcutsSection *self = (IdeShortcutsSection *)object;
+
+  switch (prop_id)
+    {
+    case PROP_SECTION_NAME:
+      g_value_set_string (value, self->name);
+      break;
+
+    case PROP_VIEW_NAME:
+      g_value_set_string (value, self->view_name);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+
+    case PROP_MAX_HEIGHT:
+      g_value_set_uint (value, self->max_height);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_section_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeShortcutsSection *self = (IdeShortcutsSection *)object;
+
+  switch (prop_id)
+    {
+    case PROP_SECTION_NAME:
+      g_free (self->name);
+      self->name = g_value_dup_string (value);
+      break;
+
+    case PROP_VIEW_NAME:
+      ide_shortcuts_section_set_view_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      g_free (self->title);
+      self->title = g_value_dup_string (value);
+      break;
+
+    case PROP_MAX_HEIGHT:
+      ide_shortcuts_section_set_max_height (self, g_value_get_uint (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static GType
+ide_shortcuts_section_child_type (GtkContainer *container)
+{
+  return GTK_TYPE_SHORTCUTS_GROUP;
+}
+
+static void
+ide_shortcuts_section_class_init (IdeShortcutsSectionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  GtkBindingSet *binding_set;
+
+  object_class->finalize = ide_shortcuts_section_finalize;
+  object_class->get_property = ide_shortcuts_section_get_property;
+  object_class->set_property = ide_shortcuts_section_set_property;
+
+  widget_class->map = ide_shortcuts_section_map;
+  widget_class->unmap = ide_shortcuts_section_unmap;
+  widget_class->destroy = ide_shortcuts_section_destroy;
+
+  container_class->add = ide_shortcuts_section_add;
+  container_class->remove = ide_shortcuts_section_remove;
+  container_class->forall = ide_shortcuts_section_forall;
+  container_class->child_type = ide_shortcuts_section_child_type;
+
+  klass->change_current_page = ide_shortcuts_section_change_current_page;
+
+  /**
+   * IdeShortcutsSection:section-name:
+   *
+   * A unique name to identify this section among the sections
+   * added to the IdeShortcutsWindow. Setting the #IdeShortcutsWindow:section-name
+   * property to this string will make this section shown in the
+   * IdeShortcutsWindow.
+   */
+  properties[PROP_SECTION_NAME] =
+    g_param_spec_string ("section-name", _("Section Name"), _("Section Name"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsSection:view-name:
+   *
+   * A view name to filter the groups in this section by.
+   * See #IdeShortcutsGroup:view.
+   *
+   * Applications are expected to use the #IdeShortcutsWindow:view-name
+   * property for this purpose.
+   */
+  properties[PROP_VIEW_NAME] =
+    g_param_spec_string ("view-name", _("View Name"), _("View Name"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));
+
+  /**
+   * IdeShortcutsSection:title:
+   *
+   * The string to show in the section selector of the IdeShortcutsWindow
+   * for this section. If there is only one section, you don't need to
+   * set a title, since the section selector will not be shown in this case.
+   */
+  properties[PROP_TITLE] =
+    g_param_spec_string ("title", _("Title"), _("Title"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsSection:max-height:
+   *
+   * The maximum number of lines to allow per column. This property can
+   * be used to influence how the groups in this section are distributed
+   * across pages and columns. The default value of 15 should work in
+   * for most cases.
+   */
+  properties[PROP_MAX_HEIGHT] =
+    g_param_spec_uint ("max-height", _("Maximum Height"), _("Maximum Height"),
+                       0, G_MAXUINT, 15,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals[CHANGE_CURRENT_PAGE] =
+    g_signal_new (_("change-current-page"),
+                  G_TYPE_FROM_CLASS (object_class),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  G_STRUCT_OFFSET (IdeShortcutsSectionClass, change_current_page),
+                  NULL, NULL, NULL,
+                  G_TYPE_BOOLEAN, 1, G_TYPE_INT);
+
+  binding_set = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (binding_set,
+                                GDK_KEY_Page_Up, 0,
+                                "change-current-page", 1,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set,
+                                GDK_KEY_Page_Down, 0,
+                                "change-current-page", 1,
+                                G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (binding_set,
+                                GDK_KEY_Page_Up, GDK_CONTROL_MASK,
+                                "change-current-page", 1,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set,
+                                GDK_KEY_Page_Down, GDK_CONTROL_MASK,
+                                "change-current-page", 1,
+                                G_TYPE_INT, 1);
+}
+
+static void
+ide_shortcuts_section_init (IdeShortcutsSection *self)
+{
+  self->max_height = 15;
+
+  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,
+                              "transition-type", GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT,
+                              "vexpand", TRUE,
+                              "visible", TRUE,
+                              NULL);
+  GTK_CONTAINER_CLASS (ide_shortcuts_section_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(self->stack));
+
+  self->switcher = g_object_new (GTK_TYPE_STACK_SWITCHER,
+                                 "halign", GTK_ALIGN_CENTER,
+                                 "stack", self->stack,
+                                 "spacing", 12,
+                                 "no-show-all", TRUE,
+                                 NULL);
+
+  gtk_style_context_remove_class (gtk_widget_get_style_context (GTK_WIDGET (self->switcher)), 
GTK_STYLE_CLASS_LINKED);
+
+  self->show_all = gtk_button_new_with_mnemonic (_("_Show All"));
+  gtk_widget_set_no_show_all (self->show_all, TRUE);
+  g_signal_connect_swapped (self->show_all, "clicked",
+                            G_CALLBACK (ide_shortcuts_section_show_all), self);
+
+  self->footer = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20);
+  GTK_CONTAINER_CLASS (ide_shortcuts_section_parent_class)->add (GTK_CONTAINER (self), self->footer);
+
+  gtk_box_set_center_widget (GTK_BOX (self->footer), GTK_WIDGET (self->switcher));
+  gtk_box_pack_end (GTK_BOX (self->footer), self->show_all, TRUE, TRUE, 0);
+  gtk_widget_set_halign (self->show_all, GTK_ALIGN_END);
+
+  self->pan_gesture = gtk_gesture_pan_new (GTK_WIDGET (self->stack), GTK_ORIENTATION_HORIZONTAL);
+  g_signal_connect (self->pan_gesture, "pan",
+                    G_CALLBACK (ide_shortcuts_section_pan_gesture_pan), self);
+}
+
+static void
+ide_shortcuts_section_set_view_name (IdeShortcutsSection *self,
+                                     const gchar         *view_name)
+{
+  if (g_strcmp0 (self->view_name, view_name) == 0)
+    return;
+
+  g_free (self->view_name);
+  self->view_name = g_strdup (view_name);
+
+  ide_shortcuts_section_filter_groups (self);
+  ide_shortcuts_section_reflow_groups (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VIEW_NAME]);
+}
+
+static void
+ide_shortcuts_section_set_max_height (IdeShortcutsSection *self,
+                                      guint                max_height)
+{
+  if (self->max_height == max_height)
+    return;
+
+  self->max_height = max_height;
+
+  ide_shortcuts_section_maybe_reflow (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_HEIGHT]);
+}
+
+static void
+ide_shortcuts_section_add_group (IdeShortcutsSection *self,
+                                 IdeShortcutsGroup   *group)
+{
+  GList *children;
+  GtkWidget *page, *column;
+
+  children = gtk_container_get_children (GTK_CONTAINER (self->stack));
+  if (children)
+    page = g_list_last (children)->data;
+  else
+    {
+      page = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 22);
+      gtk_stack_add_named (self->stack, page, "1");
+    }
+  g_list_free (children);
+
+  children = gtk_container_get_children (GTK_CONTAINER (page));
+  if (children)
+    column = g_list_last (children)->data;
+  else
+    {
+      column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22);
+      gtk_container_add (GTK_CONTAINER (page), column);
+    }
+  g_list_free (children);
+
+  gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (group));
+  self->groups = g_list_append (self->groups, group);
+
+  ide_shortcuts_section_maybe_reflow (self);
+}
+
+static void
+ide_shortcuts_section_show_all (IdeShortcutsSection *self)
+{
+  ide_shortcuts_section_set_view_name (self, NULL);
+}
+
+static void
+update_group_visibility (GtkWidget *child, gpointer data)
+{
+  IdeShortcutsSection *self = data;
+
+  if (IDE_IS_SHORTCUTS_GROUP (child))
+    {
+      gchar *view;
+      gboolean match;
+
+      g_object_get (child, "view", &view, NULL);
+      match = view == NULL ||
+              self->view_name == NULL ||
+              strcmp (view, self->view_name) == 0;
+
+      gtk_widget_set_visible (child, match);
+      self->has_filtered_group |= !match;
+
+      g_free (view);
+    }
+  else if (GTK_IS_CONTAINER (child))
+    {
+      gtk_container_foreach (GTK_CONTAINER (child), update_group_visibility, data);
+    }
+}
+
+static void
+ide_shortcuts_section_filter_groups (IdeShortcutsSection *self)
+{
+  self->has_filtered_group = FALSE;
+
+  gtk_container_foreach (GTK_CONTAINER (self), update_group_visibility, self);
+
+  gtk_widget_set_visible (GTK_WIDGET (self->show_all), self->has_filtered_group);
+  gtk_widget_set_visible (gtk_widget_get_parent (GTK_WIDGET (self->show_all)),
+                          gtk_widget_get_visible (GTK_WIDGET (self->show_all)) ||
+                          gtk_widget_get_visible (GTK_WIDGET (self->switcher)));
+}
+
+static void
+ide_shortcuts_section_maybe_reflow (IdeShortcutsSection *self)
+{
+  if (gtk_widget_get_mapped (GTK_WIDGET (self)))
+    ide_shortcuts_section_reflow_groups (self);
+  else
+    self->need_reflow = TRUE;
+}
+
+static void
+adjust_page_buttons (GtkWidget *widget,
+                     gpointer   data)
+{
+  GtkWidget *label;
+
+  gtk_style_context_add_class (gtk_widget_get_style_context (widget), "circular");
+
+  label = gtk_bin_get_child (GTK_BIN (widget));
+  gtk_label_set_use_underline (GTK_LABEL (label), TRUE);
+}
+
+static void
+ide_shortcuts_section_reflow_groups (IdeShortcutsSection *self)
+{
+  GList *pages, *p;
+  GList *columns, *c;
+  GList *groups, *g;
+  GList *children;
+  guint n_rows;
+  guint n_columns;
+  guint n_pages;
+  GtkWidget *current_page, *current_column;
+
+  /* collect all groups from the current pages */
+  groups = NULL;
+  pages = gtk_container_get_children (GTK_CONTAINER (self->stack));
+  for (p = pages; p; p = p->next)
+    {
+      columns = gtk_container_get_children (GTK_CONTAINER (p->data));
+      for (c = columns; c; c = c->next)
+        {
+          children = gtk_container_get_children (GTK_CONTAINER (c->data));
+          groups = g_list_concat (groups, children);
+        }
+      g_list_free (columns);
+    }
+  g_list_free (pages);
+
+  /* create new pages */
+  current_page = NULL;
+  current_column = NULL;
+  pages = NULL;
+  n_rows = 0;
+  n_columns = 0;
+  n_pages = 0;
+  for (g = groups; g; g = g->next)
+    {
+      IdeShortcutsGroup *group = g->data;
+      guint height;
+      gboolean visible;
+
+      g_object_get (group,
+                    "visible", &visible,
+                    "height", &height,
+                    NULL);
+      if (!visible)
+        height = 0;
+
+      if (current_column == NULL || n_rows + height > self->max_height)
+        {
+          GtkWidget *column;
+          GtkSizeGroup *size_group;
+
+          column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22);
+          gtk_widget_show (column);
+
+          size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+          gtk_size_group_set_ignore_hidden (size_group, TRUE);
+G_GNUC_END_IGNORE_DEPRECATIONS
+          g_object_set_data_full (G_OBJECT (column), "accel-size-group", size_group, g_object_unref);
+
+          size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+          gtk_size_group_set_ignore_hidden (size_group, TRUE);
+G_GNUC_END_IGNORE_DEPRECATIONS
+          g_object_set_data_full (G_OBJECT (column), "title-size-group", size_group, g_object_unref);
+
+          if (n_columns % 2 == 0)
+            {
+              GtkWidget *page;
+
+              page = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 22);
+              gtk_widget_show (page);
+
+              pages = g_list_append (pages, page);
+              current_page = page;
+            }
+
+          gtk_container_add (GTK_CONTAINER (current_page), column);
+          current_column = column;
+          n_columns += 1;
+          n_rows = 0;
+        }
+
+      n_rows += height;
+
+      g_object_set (group,
+                    "accel-size-group", g_object_get_data (G_OBJECT (current_column), "accel-size-group"),
+                    "title-size-group", g_object_get_data (G_OBJECT (current_column), "title-size-group"),
+                    NULL);
+
+      g_object_ref (group);
+      gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (group))), GTK_WIDGET (group));
+      gtk_container_add (GTK_CONTAINER (current_column), GTK_WIDGET (group));
+      g_object_unref (group);
+    }
+
+  /* balance the last page */
+  if (n_columns % 2 == 1)
+    {
+      GtkWidget *column;
+      GtkSizeGroup *size_group;
+      GList *content;
+      guint n;
+
+      column = gtk_box_new (GTK_ORIENTATION_VERTICAL, 22);
+      gtk_widget_show (column);
+
+      size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+      gtk_size_group_set_ignore_hidden (size_group, TRUE);
+G_GNUC_END_IGNORE_DEPRECATIONS
+      g_object_set_data_full (G_OBJECT (column), "accel-size-group", size_group, g_object_unref);
+      size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+      gtk_size_group_set_ignore_hidden (size_group, TRUE);
+G_GNUC_END_IGNORE_DEPRECATIONS
+      g_object_set_data_full (G_OBJECT (column), "title-size-group", size_group, g_object_unref);
+
+      gtk_container_add (GTK_CONTAINER (current_page), column);
+
+      content = gtk_container_get_children (GTK_CONTAINER (current_column));
+      n = 0;
+
+      for (g = g_list_last (content); g; g = g->prev)
+        {
+          IdeShortcutsGroup *group = g->data;
+          guint height;
+          gboolean visible;
+
+          g_object_get (group,
+                        "visible", &visible,
+                        "height", &height,
+                        NULL);
+          if (!visible)
+            height = 0;
+
+          if (n_rows - height == 0)
+            break;
+          if (ABS ((gint)n_rows - (gint)n) < ABS (((gint)n_rows - (gint)height) - ((gint)n + (gint)height)))
+            break;
+
+          n_rows -= height;
+          n += height;
+        }
+
+      for (g = g->next; g; g = g->next)
+        {
+          IdeShortcutsGroup *group = g->data;
+
+          g_object_set (group,
+                        "accel-size-group", g_object_get_data (G_OBJECT (column), "accel-size-group"),
+                        "title-size-group", g_object_get_data (G_OBJECT (column), "title-size-group"),
+                        NULL);
+
+          g_object_ref (group);
+          gtk_container_remove (GTK_CONTAINER (current_column), GTK_WIDGET (group));
+          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (group));
+          g_object_unref (group);
+        }
+
+      g_list_free (content);
+    }
+
+  /* replace the current pages with the new pages */
+  children = gtk_container_get_children (GTK_CONTAINER (self->stack));
+  g_list_free_full (children, (GDestroyNotify)gtk_widget_destroy);
+
+  for (p = pages, n_pages = 0; p; p = p->next, n_pages++)
+    {
+      GtkWidget *page = p->data;
+      gchar *title;
+
+      title = g_strdup_printf ("_%u", n_pages + 1);
+      gtk_stack_add_titled (self->stack, page, title, title);
+      g_free (title);
+    }
+
+  /* fix up stack switcher */
+  gtk_container_foreach (GTK_CONTAINER (self->switcher), adjust_page_buttons, NULL);
+  gtk_widget_set_visible (GTK_WIDGET (self->switcher), (n_pages > 1));
+  gtk_widget_set_visible (gtk_widget_get_parent (GTK_WIDGET (self->switcher)),
+                          gtk_widget_get_visible (GTK_WIDGET (self->show_all)) ||
+                          gtk_widget_get_visible (GTK_WIDGET (self->switcher)));
+
+  /* clean up */
+  g_list_free (groups);
+  g_list_free (pages);
+
+  self->need_reflow = FALSE;
+}
+
+static gboolean
+ide_shortcuts_section_change_current_page (IdeShortcutsSection *self,
+                                           gint                 offset)
+{
+  GtkWidget *child;
+  GList *children, *l;
+
+  child = gtk_stack_get_visible_child (self->stack);
+  children = gtk_container_get_children (GTK_CONTAINER (self->stack));
+  l = g_list_find (children, child);
+
+  if (offset == 1)
+    l = l->next;
+  else if (offset == -1)
+    l = l->prev;
+  else
+    g_assert_not_reached ();
+
+  if (l)
+    gtk_stack_set_visible_child (self->stack, GTK_WIDGET (l->data));
+  else
+    gtk_widget_error_bell (GTK_WIDGET (self));
+
+  g_list_free (children);
+
+  return TRUE;
+}
+
+static void
+ide_shortcuts_section_pan_gesture_pan (GtkGesturePan       *gesture,
+                                       GtkPanDirection      direction,
+                                       gdouble              offset,
+                                       IdeShortcutsSection *self)
+{
+  if (offset < 50)
+    return;
+
+  if (direction == GTK_PAN_DIRECTION_LEFT)
+    ide_shortcuts_section_change_current_page (self, 1);
+  else if (direction == GTK_PAN_DIRECTION_RIGHT)
+    ide_shortcuts_section_change_current_page (self, -1);
+  else
+    g_assert_not_reached ();
+
+  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
+}
diff --git a/libide/shortcuts/ide-shortcuts-section.h b/libide/shortcuts/ide-shortcuts-section.h
new file mode 100644
index 0000000..8783e1d
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-section.h
@@ -0,0 +1,40 @@
+/* ide-shortcuts-section.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __IDE_SHORTCUTS_SECTION_H__
+#define __IDE_SHORTCUTS_SECTION_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_SECTION            (ide_shortcuts_section_get_type ())
+#define IDE_SHORTCUTS_SECTION(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
IDE_TYPE_SHORTCUTS_SECTION, IdeShortcutsSection))
+#define IDE_SHORTCUTS_SECTION_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_SHORTCUTS_SECTION, 
IdeShortcutsSectionClass))
+#define IDE_IS_SHORTCUTS_SECTION(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
IDE_TYPE_SHORTCUTS_SECTION))
+#define IDE_IS_SHORTCUTS_SECTION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_SHORTCUTS_SECTION))
+#define IDE_SHORTCUTS_SECTION_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_SHORTCUTS_SECTION, 
IdeShortcutsSectionClass))
+
+typedef struct _IdeShortcutsSection      IdeShortcutsSection;
+typedef struct _IdeShortcutsSectionClass IdeShortcutsSectionClass;
+
+GType ide_shortcuts_section_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* __IDE_SHORTCUTS_SECTION_H__ */
diff --git a/libide/shortcuts/ide-shortcuts-shortcut-private.h 
b/libide/shortcuts/ide-shortcuts-shortcut-private.h
new file mode 100644
index 0000000..3a4dc11
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-shortcut-private.h
@@ -0,0 +1,37 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000.  See the AUTHORS
+ * file for a list of people on the GTK+ Team.  See the ChangeLog
+ * files for a list of changes.  These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+#ifndef __IDE_SHORTCUTS_SHORTCUT_PRIVATE_H__
+#define __IDE_SHORTCUTS_SHORTCUT_PRIVATE_H__
+
+#include "ide-shortcuts-shortcut.h"
+
+G_BEGIN_DECLS
+
+void ide_shortcuts_shortcut_update_accel (IdeShortcutsShortcut *self,
+                                          GtkWindow            *window);
+
+G_END_DECLS
+
+#endif /* __GTK_sHORTCUTS_SHORTCUT_PRIVATE_H__ */
diff --git a/libide/shortcuts/ide-shortcuts-shortcut.c b/libide/shortcuts/ide-shortcuts-shortcut.c
new file mode 100644
index 0000000..d8312ec
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-shortcut.c
@@ -0,0 +1,730 @@
+/* ide-shortcuts-shortcut.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcut-label.h"
+#include "ide-shortcuts-shortcut.h"
+#include "ide-shortcuts-window-private.h"
+
+/**
+ * SECTION:ide-shortcuts-shortcut
+ * @Title: IdeShortcutsShortcut
+ * @Short_description: Represents a keyboard shortcut in a IdeShortcutsWindow
+ *
+ * A IdeShortcutsShortcut represents a single keyboard shortcut or gesture
+ * with a short text. This widget is only meant to be used with #IdeShortcutsWindow.
+ */
+
+struct _IdeShortcutsShortcut
+{
+  GtkBox            parent_instance;
+
+  GtkImage         *image;
+  IdeShortcutLabel *accelerator;
+  GtkLabel         *title;
+  GtkLabel         *subtitle;
+  GtkLabel         *title_box;
+
+  GtkSizeGroup     *accel_size_group;
+  GtkSizeGroup     *title_size_group;
+
+  gboolean          subtitle_set;
+  gboolean          icon_set;
+  GtkTextDirection  direction;
+  gchar            *action_name;
+  GtkShortcutType   shortcut_type;
+};
+
+struct _IdeShortcutsShortcutClass
+{
+  GtkBoxClass parent_class;
+};
+
+G_DEFINE_TYPE (IdeShortcutsShortcut, ide_shortcuts_shortcut, GTK_TYPE_BOX)
+
+enum {
+  PROP_0,
+  PROP_ACCELERATOR,
+  PROP_ICON,
+  PROP_ICON_SET,
+  PROP_TITLE,
+  PROP_SUBTITLE,
+  PROP_SUBTITLE_SET,
+  PROP_ACCEL_SIZE_GROUP,
+  PROP_TITLE_SIZE_GROUP,
+  PROP_DIRECTION,
+  PROP_SHORTCUT_TYPE,
+  PROP_ACTION_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP];
+
+static void
+ide_shortcuts_shortcut_set_accelerator (IdeShortcutsShortcut *self,
+                                        const gchar          *accelerator)
+{
+  ide_shortcut_label_set_accelerator (self->accelerator, accelerator);
+}
+
+static void
+ide_shortcuts_shortcut_set_accel_size_group (IdeShortcutsShortcut *self,
+                                             GtkSizeGroup         *group)
+{
+  if (self->accel_size_group)
+    {
+      gtk_size_group_remove_widget (self->accel_size_group, GTK_WIDGET (self->accelerator));
+      gtk_size_group_remove_widget (self->accel_size_group, GTK_WIDGET (self->image));
+    }
+
+  if (group)
+    {
+      gtk_size_group_add_widget (group, GTK_WIDGET (self->accelerator));
+      gtk_size_group_add_widget (group, GTK_WIDGET (self->image));
+    }
+
+  g_set_object (&self->accel_size_group, group);
+}
+
+static void
+ide_shortcuts_shortcut_set_title_size_group (IdeShortcutsShortcut *self,
+                                             GtkSizeGroup         *group)
+{
+  if (self->title_size_group)
+    gtk_size_group_remove_widget (self->title_size_group, GTK_WIDGET (self->title_box));
+  if (group)
+    gtk_size_group_add_widget (group, GTK_WIDGET (self->title_box));
+
+  g_set_object (&self->title_size_group, group);
+}
+
+static void
+update_subtitle_from_type (IdeShortcutsShortcut *self)
+{
+  const gchar *subtitle;
+
+  if (self->subtitle_set)
+    return;
+
+  switch (self->shortcut_type)
+    {
+    case GTK_SHORTCUT_ACCELERATOR:
+    case GTK_SHORTCUT_GESTURE:
+      subtitle = NULL;
+      break;
+
+    case GTK_SHORTCUT_GESTURE_PINCH:
+      subtitle = _("Two finger pinch");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_STRETCH:
+      subtitle = _("Two finger stretch");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_ROTATE_CLOCKWISE:
+      subtitle = _("Rotate clockwise");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE:
+      subtitle = _("Rotate counterclockwise");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT:
+      subtitle = _("Two finger swipe left");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT:
+      subtitle = _("Two finger swipe right");
+      break;
+
+    default:
+      subtitle = NULL;
+      break;
+    }
+
+  gtk_label_set_label (self->subtitle, subtitle);
+  gtk_widget_set_visible (GTK_WIDGET (self->subtitle), subtitle != NULL);
+  g_object_notify (G_OBJECT (self), "subtitle");
+}
+
+static void
+ide_shortcuts_shortcut_set_subtitle_set (IdeShortcutsShortcut *self,
+                                         gboolean              subtitle_set)
+{
+  if (self->subtitle_set != subtitle_set)
+    {
+      self->subtitle_set = subtitle_set;
+      g_object_notify (G_OBJECT (self), "subtitle-set");
+    }
+  update_subtitle_from_type (self);
+}
+
+static void
+ide_shortcuts_shortcut_set_subtitle (IdeShortcutsShortcut *self,
+                                     const gchar          *subtitle)
+{
+  gtk_label_set_label (self->subtitle, subtitle);
+  gtk_widget_set_visible (GTK_WIDGET (self->subtitle), subtitle && subtitle[0]);
+  ide_shortcuts_shortcut_set_subtitle_set (self, subtitle && subtitle[0]);
+
+  g_object_notify (G_OBJECT (self), "subtitle");
+}
+
+static void
+update_icon_from_type (IdeShortcutsShortcut *self)
+{
+  GIcon *icon;
+
+  if (self->icon_set)
+    return;
+
+  switch (self->shortcut_type)
+    {
+    case GTK_SHORTCUT_GESTURE_PINCH:
+      icon = g_themed_icon_new ("gesture-pinch-symbolic");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_STRETCH:
+      icon = g_themed_icon_new ("gesture-stretch-symbolic");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_ROTATE_CLOCKWISE:
+      icon = g_themed_icon_new ("gesture-rotate-clockwise-symbolic");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE:
+      icon = g_themed_icon_new ("gesture-rotate-anticlockwise-symbolic");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT:
+      icon = g_themed_icon_new ("gesture-two-finger-swipe-left-symbolic");
+      break;
+
+    case GTK_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT:
+      icon = g_themed_icon_new ("gesture-two-finger-swipe-right-symbolic");
+      break;
+
+    case GTK_SHORTCUT_ACCELERATOR:
+    case GTK_SHORTCUT_GESTURE:
+    default: ;
+      icon = NULL;
+      break;
+    }
+
+  if (icon)
+    {
+      gtk_image_set_from_gicon (self->image, icon, GTK_ICON_SIZE_DIALOG);
+      gtk_image_set_pixel_size (self->image, 64);
+      g_object_unref (icon);
+    }
+}
+
+static void
+ide_shortcuts_shortcut_set_icon_set (IdeShortcutsShortcut *self,
+                                     gboolean              icon_set)
+{
+  if (self->icon_set != icon_set)
+    {
+      self->icon_set = icon_set;
+      g_object_notify (G_OBJECT (self), "icon-set");
+    }
+  update_icon_from_type (self);
+}
+
+static void
+ide_shortcuts_shortcut_set_icon (IdeShortcutsShortcut *self,
+                                 GIcon                *gicon)
+{
+  gtk_image_set_from_gicon (self->image, gicon, GTK_ICON_SIZE_DIALOG);
+  ide_shortcuts_shortcut_set_icon_set (self, gicon != NULL);
+  g_object_notify (G_OBJECT (self), "icon");
+}
+
+static void
+update_visible_from_direction (IdeShortcutsShortcut *self)
+{
+  if (self->direction == GTK_TEXT_DIR_NONE ||
+      self->direction == gtk_widget_get_direction (GTK_WIDGET (self)))
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self), TRUE);
+      gtk_widget_set_no_show_all (GTK_WIDGET (self), FALSE);
+    }
+  else
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self), FALSE);
+      gtk_widget_set_no_show_all (GTK_WIDGET (self), TRUE);
+    }
+}
+
+static void
+ide_shortcuts_shortcut_set_direction (IdeShortcutsShortcut *self,
+                                      GtkTextDirection      direction)
+{
+  if (self->direction == direction)
+    return;
+
+  self->direction = direction;
+
+  update_visible_from_direction (self);
+
+  g_object_notify (G_OBJECT (self), "direction");
+}
+
+static void
+ide_shortcuts_shortcut_direction_changed (GtkWidget        *widget,
+                                          GtkTextDirection  previous_dir)
+{
+  update_visible_from_direction (IDE_SHORTCUTS_SHORTCUT (widget));
+
+  GTK_WIDGET_CLASS (ide_shortcuts_shortcut_parent_class)->direction_changed (widget, previous_dir);
+}
+
+static void
+ide_shortcuts_shortcut_set_type (IdeShortcutsShortcut *self,
+                                 GtkShortcutType       type)
+{
+  if (self->shortcut_type == type)
+    return;
+
+  self->shortcut_type = type;
+
+  update_subtitle_from_type (self);
+  update_icon_from_type (self);
+
+  gtk_widget_set_visible (GTK_WIDGET (self->accelerator), type == GTK_SHORTCUT_ACCELERATOR);
+  gtk_widget_set_visible (GTK_WIDGET (self->image), type != GTK_SHORTCUT_ACCELERATOR);
+
+
+  g_object_notify (G_OBJECT (self), "shortcut-type");
+}
+
+static void
+ide_shortcuts_shortcut_set_action_name (IdeShortcutsShortcut *self,
+                                        const gchar          *action_name)
+{
+  g_free (self->action_name);
+  self->action_name = g_strdup (action_name);
+
+  g_object_notify (G_OBJECT (self), "action-name");
+}
+
+static void
+ide_shortcuts_shortcut_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeShortcutsShortcut *self = IDE_SHORTCUTS_SHORTCUT (object);
+
+  switch (prop_id)
+    {
+    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;
+
+    case PROP_SUBTITLE_SET:
+      g_value_set_boolean (value, self->subtitle_set);
+      break;
+
+    case PROP_ACCELERATOR:
+      g_value_set_string (value, ide_shortcut_label_get_accelerator (self->accelerator));
+      break;
+
+    case PROP_ICON:
+      {
+        GIcon *icon;
+
+        gtk_image_get_gicon (self->image, &icon, NULL);
+        g_value_set_object (value, icon);
+      }
+      break;
+
+    case PROP_ICON_SET:
+      g_value_set_boolean (value, self->icon_set);
+      break;
+
+    case PROP_DIRECTION:
+      g_value_set_enum (value, self->direction);
+      break;
+
+    case PROP_SHORTCUT_TYPE:
+      g_value_set_enum (value, self->shortcut_type);
+      break;
+
+    case PROP_ACTION_NAME:
+      g_value_set_string (value, self->action_name);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_shortcut_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeShortcutsShortcut *self = IDE_SHORTCUTS_SHORTCUT (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCELERATOR:
+      ide_shortcuts_shortcut_set_accelerator (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON:
+      ide_shortcuts_shortcut_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_ICON_SET:
+      ide_shortcuts_shortcut_set_icon_set (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ACCEL_SIZE_GROUP:
+      ide_shortcuts_shortcut_set_accel_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value)));
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE:
+      ide_shortcuts_shortcut_set_subtitle (self, g_value_get_string (value));
+      break;
+
+    case PROP_SUBTITLE_SET:
+      ide_shortcuts_shortcut_set_subtitle_set (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE_SIZE_GROUP:
+      ide_shortcuts_shortcut_set_title_size_group (self, GTK_SIZE_GROUP (g_value_get_object (value)));
+      break;
+
+    case PROP_DIRECTION:
+      ide_shortcuts_shortcut_set_direction (self, g_value_get_enum (value));
+      break;
+
+    case PROP_SHORTCUT_TYPE:
+      ide_shortcuts_shortcut_set_type (self, g_value_get_enum (value));
+      break;
+
+    case PROP_ACTION_NAME:
+      ide_shortcuts_shortcut_set_action_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+ide_shortcuts_shortcut_finalize (GObject *object)
+{
+  IdeShortcutsShortcut *self = IDE_SHORTCUTS_SHORTCUT (object);
+
+  g_clear_object (&self->accel_size_group);
+  g_clear_object (&self->title_size_group);
+  g_free (self->action_name);
+
+  G_OBJECT_CLASS (ide_shortcuts_shortcut_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcuts_shortcut_add (GtkContainer *container,
+                            GtkWidget    *widget)
+{
+  g_warning ("Can't add children to %s", G_OBJECT_TYPE_NAME (container));
+}
+
+static GType
+ide_shortcuts_shortcut_child_type (GtkContainer *container)
+{
+  return G_TYPE_NONE;
+}
+
+void
+ide_shortcuts_shortcut_update_accel (IdeShortcutsShortcut *self,
+                                     GtkWindow            *window)
+{
+  GtkApplication *app;
+  gchar **accels;
+  gchar *str;
+
+  if (self->action_name == NULL)
+    return;
+
+  app = gtk_window_get_application (window);
+  if (app == NULL)
+    return;
+
+  accels = gtk_application_get_accels_for_action (app, self->action_name);
+  str = g_strjoinv (" ", accels);
+
+  ide_shortcuts_shortcut_set_accelerator (self, str);
+
+  g_free (str);
+  g_strfreev (accels);
+}
+
+static void
+ide_shortcuts_shortcut_class_init (IdeShortcutsShortcutClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = ide_shortcuts_shortcut_finalize;
+  object_class->get_property = ide_shortcuts_shortcut_get_property;
+  object_class->set_property = ide_shortcuts_shortcut_set_property;
+
+  widget_class->direction_changed = ide_shortcuts_shortcut_direction_changed;
+
+  container_class->add = ide_shortcuts_shortcut_add;
+  container_class->child_type = ide_shortcuts_shortcut_child_type;
+
+  /**
+   * IdeShortcutsShortcut:accelerator:
+   *
+   * The accelerator(s) represented by this object. This property is used
+   * if #IdeShortcutsShortcut:shortcut-type is set to #GTK_SHORTCUT_ACCELERATOR.
+   *
+   * The syntax of this property is (an extension of) the syntax understood by
+   * gtk_accelerator_parse(). Multiple accelerators can be specified by separating
+   * them with a space, but keep in mind that the available width is limited.
+   * It is also possible to specify ranges of shortcuts, using ... between the keys.
+   * Sequences of keys can be specified using a + or & between the keys.
+   *
+   * Examples:
+   * - A single shortcut: <ctl><alt>delete
+   * - Two alternative shortcuts: <shift>a Home
+   * - A range of shortcuts: <alt>1...<alt>9
+   * - Several keys pressed together: Control_L&Control_R
+   * - A sequence of shortcuts or keys: <ctl>c+<ctl>x
+   *
+   * Use + instead of & when the keys may (or have to be) pressed sequentially (e.g
+   * use t+t for 'press the t key twice').
+   *
+   * Note that <, > and & need to be escaped as &lt;, &gt; and &amp; when used
+   * in .ui files.
+   */
+  properties[PROP_ACCELERATOR] =
+    g_param_spec_string ("accelerator",
+                         "Accelerator",
+                         "The accelerator keys for shortcuts of type 'Accelerator'",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:icon:
+   *
+   * An icon to represent the shortcut or gesture. This property is used if
+   * #IdeShortcutsShortcut:shortcut-type is set to #GTK_SHORTCUT_GESTURE.
+   * For the other predefined gesture types, GTK+ provides an icon on its own.
+   */
+  properties[PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "The icon to show for shortcuts of type 'Other Gesture'",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:icon-set:
+   *
+   * %TRUE if an icon has been set.
+   */
+  properties[PROP_ICON_SET] =
+    g_param_spec_boolean ("icon-set",
+                          "Icon Set",
+                          "Whether an icon has been set",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:title:
+   *
+   * The textual description for the shortcut or gesture represented by
+   * this object. This should be a short string that can fit in a single line.
+   */
+  properties[PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "A short description for the shortcut",
+                         "",
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:subtitle:
+   *
+   * The subtitle for the shortcut or gesture.
+   *
+   * This is typically used for gestures and should be a short, one-line
+   * text that describes the gesture itself. For the predefined gesture
+   * types, GTK+ provides a subtitle on its own.
+   */
+  properties[PROP_SUBTITLE] =
+    g_param_spec_string ("subtitle",
+                         "Subtitle",
+                         "A short description for the gesture",
+                         "",
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:subtitle-set:
+   *
+   * %TRUE if a subtitle has been set.
+   */
+  properties[PROP_SUBTITLE_SET] =
+    g_param_spec_boolean ("subtitle-set",
+                          "Subtitle Set",
+                          "Whether a subtitle has been set",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:accel-size-group:
+   *
+   * The size group for the accelerator portion of this shortcut.
+   *
+   * This is used internally by GTK+, and must not be modified by applications.
+   */
+  properties[PROP_ACCEL_SIZE_GROUP] =
+    g_param_spec_object ("accel-size-group",
+                         "Accelerator Size Group",
+                         "Accelerator Size Group",
+                         GTK_TYPE_SIZE_GROUP,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsShortcut:title-size-group:
+   *
+   * The size group for the textual portion of this shortcut.
+   *
+   * This is used internally by GTK+, and must not be modified by applications.
+   */
+  properties[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));
+
+  /**
+   * IdeShortcutsShortcut:direction:
+   *
+   * The text direction for which this shortcut is active. If the shortcut
+   * is used regardless of the text direction, set this property to
+   * #GTK_TEXT_DIR_NONE.
+   */
+  properties[PROP_DIRECTION] =
+    g_param_spec_enum ("direction",
+                       "Direction",
+                       "Text direction for which this shortcut is active",
+                       GTK_TYPE_TEXT_DIRECTION,
+                       GTK_TEXT_DIR_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));
+
+  /**
+   * IdeShortcutsShortcut:shortcut-type:
+   *
+   * The type of shortcut that is represented.
+   */
+  properties[PROP_SHORTCUT_TYPE] =
+    g_param_spec_enum ("shortcut-type",
+                       "Shortcut Type",
+                       "The type of shortcut that is represented",
+                       GTK_TYPE_SHORTCUT_TYPE,
+                       GTK_SHORTCUT_ACCELERATOR,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY));
+
+  /**
+   * IdeShortcutsShortcut:action-name:
+   *
+   * A detailed action name. If this is set for a shortcut
+   * of type %GTK_SHORTCUT_ACCELERATOR, then GTK+ will use
+   * the accelerators that are associated with the action
+   * via gtk_application_set_accels_for_action(), and setting
+   * #IdeShortcutsShortcut::accelerator is not necessary.
+   *
+   * Since: 3.22
+   */
+  properties[PROP_ACTION_NAME] =
+    g_param_spec_string ("action-name",
+                         "Action Name",
+                         "The name of the action",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+  gtk_widget_class_set_css_name (widget_class, "shortcut");
+}
+
+static void
+ide_shortcuts_shortcut_init (IdeShortcutsShortcut *self)
+{
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_HORIZONTAL);
+  gtk_box_set_spacing (GTK_BOX (self), 12);
+
+  self->direction = GTK_TEXT_DIR_NONE;
+  self->shortcut_type = GTK_SHORTCUT_ACCELERATOR;
+
+  self->image = g_object_new (GTK_TYPE_IMAGE,
+                              "visible", FALSE,
+                              "valign", GTK_ALIGN_CENTER,
+                              "no-show-all", TRUE,
+                              NULL);
+  GTK_CONTAINER_CLASS (ide_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(self->image));
+
+  self->accelerator = g_object_new (IDE_TYPE_SHORTCUT_LABEL,
+                                    "visible", TRUE,
+                                    "valign", GTK_ALIGN_CENTER,
+                                    "no-show-all", TRUE,
+                                    NULL);
+  GTK_CONTAINER_CLASS (ide_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(self->accelerator));
+
+  self->title_box = g_object_new (GTK_TYPE_BOX,
+                                  "visible", TRUE,
+                                  "valign", GTK_ALIGN_CENTER,
+                                  "hexpand", TRUE,
+                                  "orientation", GTK_ORIENTATION_VERTICAL,
+                                  NULL);
+  GTK_CONTAINER_CLASS (ide_shortcuts_shortcut_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(self->title_box));
+
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (self->title_box), GTK_WIDGET (self->title));
+
+  self->subtitle = g_object_new (GTK_TYPE_LABEL,
+                                 "visible", FALSE,
+                                 "no-show-all", TRUE,
+                                 "xalign", 0.0f,
+                                 NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->subtitle)),
+                               GTK_STYLE_CLASS_DIM_LABEL);
+  gtk_container_add (GTK_CONTAINER (self->title_box), GTK_WIDGET (self->subtitle));
+}
diff --git a/libide/shortcuts/ide-shortcuts-shortcut.h b/libide/shortcuts/ide-shortcuts-shortcut.h
new file mode 100644
index 0000000..f742a91
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-shortcut.h
@@ -0,0 +1,78 @@
+/* ide-shortcuts-shortcutprivate.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef IDE_SHORTCUTS_SHORTCUT_H
+#define IDE_SHORTCUTS_SHORTCUT_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_SHORTCUT (ide_shortcuts_shortcut_get_type())
+#define IDE_SHORTCUTS_SHORTCUT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), 
IDE_TYPE_SHORTCUTS_SHORTCUT, IdeShortcutsShortcut))
+#define IDE_SHORTCUTS_SHORTCUT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), 
IDE_TYPE_SHORTCUTS_SHORTCUT, IdeShortcutsShortcutClass))
+#define IDE_IS_SHORTCUTS_SHORTCUT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), 
IDE_TYPE_SHORTCUTS_SHORTCUT))
+#define IDE_IS_SHORTCUTS_SHORTCUT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), 
IDE_TYPE_SHORTCUTS_SHORTCUT))
+#define IDE_SHORTCUTS_SHORTCUT_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), 
IDE_TYPE_SHORTCUTS_SHORTCUT, IdeShortcutsShortcutClass))
+
+
+typedef struct _IdeShortcutsShortcut      IdeShortcutsShortcut;
+typedef struct _IdeShortcutsShortcutClass IdeShortcutsShortcutClass;
+
+/**
+ * IdeShortcutType:
+ * @IDE_SHORTCUT_ACCELERATOR:
+ *   The shortcut is a keyboard accelerator. The #IdeShortcutsShortcut:accelerator
+ *   property will be used.
+ * @IDE_SHORTCUT_GESTURE_PINCH:
+ *   The shortcut is a pinch gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE_STRETCH:
+ *   The shortcut is a stretch gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE_ROTATE_CLOCKWISE:
+ *   The shortcut is a clockwise rotation gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE:
+ *   The shortcut is a counterclockwise rotation gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT:
+ *   The shortcut is a two-finger swipe gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT:
+ *   The shortcut is a two-finger swipe gesture. GTK+ provides an icon and subtitle.
+ * @IDE_SHORTCUT_GESTURE:
+ *   The shortcut is a gesture. The #IdeShortcutsShortcut:icon property will be
+ *   used.
+ *
+ * IdeShortcutType specifies the kind of shortcut that is being described.
+ * More values may be added to this enumeration over time.
+ *
+ * Since: 3.20
+ */
+typedef enum {
+  IDE_SHORTCUT_ACCELERATOR,
+  IDE_SHORTCUT_GESTURE_PINCH,
+  IDE_SHORTCUT_GESTURE_STRETCH,
+  IDE_SHORTCUT_GESTURE_ROTATE_CLOCKWISE,
+  IDE_SHORTCUT_GESTURE_ROTATE_COUNTERCLOCKWISE,
+  IDE_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_LEFT,
+  IDE_SHORTCUT_GESTURE_TWO_FINGER_SWIPE_RIGHT,
+  IDE_SHORTCUT_GESTURE
+} IdeShortcutType;
+
+GType ide_shortcuts_shortcut_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUTS_SHORTCUT_H */
diff --git a/libide/shortcuts/ide-shortcuts-window-private.h b/libide/shortcuts/ide-shortcuts-window-private.h
new file mode 100644
index 0000000..8c122b8
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-window-private.h
@@ -0,0 +1,38 @@
+
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000.  See the AUTHORS
+ * file for a list of people on the GTK+ Team.  See the ChangeLog
+ * files for a list of changes.  These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+#ifndef __IDE_SHORTCUTS_WINDOW_PRIVATE_H__
+#define __IDE_SHORTCUTS_WINDOW_PRIVATE_H__
+
+#include "ide-shortcuts-window.h"
+
+G_BEGIN_DECLS
+
+void ide_shortcuts_window_set_window (IdeShortcutsWindow *self,
+                                      GtkWindow          *window);
+
+G_END_DECLS
+
+#endif /* __GTK_sHORTCUTS_WINDOW_PRIVATE_H__ */
diff --git a/libide/shortcuts/ide-shortcuts-window.c b/libide/shortcuts/ide-shortcuts-window.c
new file mode 100644
index 0000000..ec7d040
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-window.c
@@ -0,0 +1,1063 @@
+/* ide-shortcuts-window.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcuts-window.h"
+#include "ide-shortcuts-section.h"
+#include "ide-shortcuts-group.h"
+#include "ide-shortcuts-shortcut-private.h"
+
+/**
+ * SECTION:ide-shortcuts-window
+ * @Title: IdeShortcutsWindow
+ * @Short_description: Toplevel which shows help for shortcuts
+ *
+ * A IdeShortcutsWindow shows brief information about the keyboard shortcuts
+ * and gestures of an application. The shortcuts can be grouped, and you can
+ * have multiple sections in this window, corresponding to the major modes of
+ * your application.
+ *
+ * Additionally, the shortcuts can be filtered by the current view, to avoid
+ * showing information that is not relevant in the current application context.
+ *
+ * The recommended way to construct a IdeShortcutsWindow is with GtkBuilder,
+ * by populating a #IdeShortcutsWindow with one or more #IdeShortcutsSection
+ * objects, which contain #IdeShortcutsGroups that in turn contain objects of
+ * class #IdeShortcutsShortcut.
+ *
+ * # A simple example:
+ *
+ * ![](gedit-shortcuts.png)
+ *
+ * This example has as single section. As you can see, the shortcut groups
+ * are arranged in columns, and spread across several pages if there are too
+ * many to find on a single page.
+ *
+ * The .ui file for this example can be found 
[here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-gedit.ui).
+ *
+ * # An example with multiple views:
+ *
+ * ![](clocks-shortcuts.png)
+ *
+ * This example shows a #IdeShortcutsWindow that has been configured to show only
+ * the shortcuts relevant to the "stopwatch" view.
+ *
+ * The .ui file for this example can be found 
[here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-clocks.ui).
+ *
+ * # An example with multiple sections:
+ *
+ * ![](builder-shortcuts.png)
+ *
+ * This example shows a #IdeShortcutsWindow with two sections, "Editor Shortcuts"
+ * and "Terminal Shortcuts".
+ *
+ * The .ui file for this example can be found 
[here](https://git.gnome.org/browse/gtk+/tree/demos/gtk-demo/shortcuts-builder.ui).
+ */
+
+typedef struct
+{
+  GHashTable     *keywords;
+  gchar          *initial_section;
+  gchar          *last_section_name;
+  gchar          *view_name;
+  GtkSizeGroup   *search_text_group;
+  GtkSizeGroup   *search_image_group;
+  GHashTable     *search_items_hash;
+
+  GtkStack       *stack;
+  GtkStack       *title_stack;
+  GtkMenuButton  *menu_button;
+  GtkLabel       *menu_label;
+  GtkSearchBar   *search_bar;
+  GtkSearchEntry *search_entry;
+  GtkHeaderBar   *header_bar;
+  GtkWidget      *main_box;
+  GtkPopover     *popover;
+  GtkListBox     *list_box;
+  GtkBox         *search_gestures;
+  GtkBox         *search_shortcuts;
+
+  GtkWindow      *window;
+  gulong          keys_changed_id;
+} IdeShortcutsWindowPrivate;
+
+typedef struct
+{
+  IdeShortcutsWindow *self;
+  GtkBuilder        *builder;
+  GQueue            *stack;
+  gchar             *property_name;
+  guint              translatable : 1;
+} ViewsParserData;
+
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeShortcutsWindow, ide_shortcuts_window, GTK_TYPE_WINDOW)
+
+
+enum {
+  CLOSE,
+  SEARCH,
+  LAST_SIGNAL
+};
+
+enum {
+  PROP_0,
+  PROP_SECTION_NAME,
+  PROP_VIEW_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP];
+static guint signals[LAST_SIGNAL];
+
+
+static gint
+number_of_children (GtkContainer *container)
+{
+  GList *children;
+  gint n;
+
+  children = gtk_container_get_children (container);
+  n = g_list_length (children);
+  g_list_free (children);
+
+  return n;
+}
+
+static void
+update_title_stack (IdeShortcutsWindow *self)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkWidget *visible_child;
+
+  visible_child = gtk_stack_get_visible_child (priv->stack);
+
+  if (IDE_IS_SHORTCUTS_SECTION (visible_child))
+    {
+      if (number_of_children (GTK_CONTAINER (priv->stack)) > 3)
+        {
+          gchar *title;
+
+          gtk_stack_set_visible_child_name (priv->title_stack, "sections");
+          g_object_get (visible_child, "title", &title, NULL);
+          gtk_label_set_label (priv->menu_label, title);
+          g_free (title);
+        }
+      else
+        {
+          gtk_stack_set_visible_child_name (priv->title_stack, "title");
+        }
+    }
+  else if (visible_child != NULL)
+    {
+      gtk_stack_set_visible_child_name (priv->title_stack, "search");
+    }
+}
+
+static void
+ide_shortcuts_window_add_search_item (GtkWidget *child, gpointer data)
+{
+  IdeShortcutsWindow *self = data;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkWidget *item;
+  gchar *accelerator = NULL;
+  gchar *title = NULL;
+  gchar *hash_key = NULL;
+  GIcon *icon = NULL;
+  gboolean icon_set = FALSE;
+  gboolean subtitle_set = FALSE;
+  GtkTextDirection direction;
+  GtkShortcutType shortcut_type;
+  gchar *action_name = NULL;
+  gchar *subtitle;
+  gchar *str;
+  gchar *keywords;
+
+  if (IDE_IS_SHORTCUTS_SHORTCUT (child))
+    {
+      GEnumClass *class;
+      GEnumValue *value;
+
+      g_object_get (child,
+                    "accelerator", &accelerator,
+                    "title", &title,
+                    "direction", &direction,
+                    "icon-set", &icon_set,
+                    "subtitle-set", &subtitle_set,
+                    "shortcut-type", &shortcut_type,
+                    "action-name", &action_name,
+                    NULL);
+
+      class = G_ENUM_CLASS (g_type_class_ref (GTK_TYPE_SHORTCUT_TYPE));
+      value = g_enum_get_value (class, shortcut_type);
+
+      hash_key = g_strdup_printf ("%s-%s-%s", title, value->value_nick, accelerator);
+
+      g_type_class_unref (class);
+
+      if (g_hash_table_contains (priv->search_items_hash, hash_key))
+        {
+          g_free (hash_key);
+          g_free (title);
+          g_free (accelerator);
+          return;
+        }
+
+      g_hash_table_insert (priv->search_items_hash, hash_key, GINT_TO_POINTER (1));
+
+      item = g_object_new (IDE_TYPE_SHORTCUTS_SHORTCUT,
+                           "accelerator", accelerator,
+                           "title", title,
+                           "direction", direction,
+                           "shortcut-type", shortcut_type,
+                           "accel-size-group", priv->search_image_group,
+                           "title-size-group", priv->search_text_group,
+                           "action-name", action_name,
+                           NULL);
+      if (icon_set)
+        {
+          g_object_get (child, "icon", &icon, NULL);
+          g_object_set (item, "icon", icon, NULL);
+          g_clear_object (&icon);
+        }
+      if (subtitle_set)
+        {
+          g_object_get (child, "subtitle", &subtitle, NULL);
+          g_object_set (item, "subtitle", subtitle, NULL);
+          g_free (subtitle);
+        }
+      str = g_strdup_printf ("%s %s", accelerator, title);
+      keywords = g_utf8_strdown (str, -1);
+
+      g_hash_table_insert (priv->keywords, item, keywords);
+      if (shortcut_type == GTK_SHORTCUT_ACCELERATOR)
+        gtk_container_add (GTK_CONTAINER (priv->search_shortcuts), item);
+      else
+        gtk_container_add (GTK_CONTAINER (priv->search_gestures), item);
+
+      g_free (title);
+      g_free (accelerator);
+      g_free (str);
+      g_free (action_name);
+    }
+  else if (GTK_IS_CONTAINER (child))
+    {
+      gtk_container_foreach (GTK_CONTAINER (child), ide_shortcuts_window_add_search_item, self);
+    }
+}
+
+static void
+section_notify_cb (GObject    *section,
+                   GParamSpec *pspec,
+                   gpointer    data)
+{
+  IdeShortcutsWindow *self = data;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (strcmp (pspec->name, "section-name") == 0)
+    {
+      gchar *name;
+
+      g_object_get (section, "section-name", &name, NULL);
+      gtk_container_child_set (GTK_CONTAINER (priv->stack), GTK_WIDGET (section), "name", name, NULL);
+      g_free (name);
+    }
+  else if (strcmp (pspec->name, "title") == 0)
+    {
+      gchar *title;
+      GtkWidget *label;
+
+      label = g_object_get_data (section, "gtk-shortcuts-title");
+      g_object_get (section, "title", &title, NULL);
+      gtk_label_set_label (GTK_LABEL (label), title);
+      g_free (title);
+    }
+}
+
+static void
+ide_shortcuts_window_add_section (IdeShortcutsWindow  *self,
+                                  IdeShortcutsSection *section)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkListBoxRow *row;
+  gchar *title;
+  gchar *name;
+  const gchar *visible_section;
+  GtkWidget *label;
+
+  gtk_container_foreach (GTK_CONTAINER (section), ide_shortcuts_window_add_search_item, self);
+
+  g_object_get (section,
+                "section-name", &name,
+                "title", &title,
+                NULL);
+
+  g_signal_connect (section, "notify", G_CALLBACK (section_notify_cb), self);
+
+  if (name == NULL)
+    name = g_strdup ("shortcuts");
+
+  gtk_stack_add_titled (priv->stack, GTK_WIDGET (section), name, title);
+
+  visible_section = gtk_stack_get_visible_child_name (priv->stack);
+  if (strcmp (visible_section, "internal-search") == 0 ||
+      (priv->initial_section && strcmp (priv->initial_section, visible_section) == 0))
+    gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (section));
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data (G_OBJECT (row), "gtk-shortcuts-section", section);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "margin", 6,
+                        "label", title,
+                        "xalign", 0.5f,
+                        "visible", TRUE,
+                        NULL);
+  g_object_set_data (G_OBJECT (section), "gtk-shortcuts-title", label);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label));
+  gtk_container_add (GTK_CONTAINER (priv->list_box), GTK_WIDGET (row));
+
+  update_title_stack (self);
+
+  g_free (name);
+  g_free (title);
+}
+
+static void
+ide_shortcuts_window_add (GtkContainer *container,
+                          GtkWidget    *widget)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)container;
+
+  if (IDE_IS_SHORTCUTS_SECTION (widget))
+    ide_shortcuts_window_add_section (self, IDE_SHORTCUTS_SECTION (widget));
+  else
+    g_warning ("Can't add children of type %s to %s",
+               G_OBJECT_TYPE_NAME (widget),
+               G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+ide_shortcuts_window_remove (GtkContainer *container,
+                             GtkWidget    *widget)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)container;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  g_signal_handlers_disconnect_by_func (widget, section_notify_cb, self);
+
+  if (widget == (GtkWidget *)priv->header_bar ||
+      widget == (GtkWidget *)priv->main_box)
+    GTK_CONTAINER_CLASS (ide_shortcuts_window_parent_class)->remove (container, widget);
+  else
+    gtk_container_remove (GTK_CONTAINER (priv->stack), widget);
+}
+
+static void
+ide_shortcuts_window_forall (GtkContainer *container,
+                             gboolean      include_internal,
+                             GtkCallback   callback,
+                             gpointer      callback_data)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)container;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (include_internal)
+    {
+      GTK_CONTAINER_CLASS (ide_shortcuts_window_parent_class)->forall (container, include_internal, 
callback, callback_data);
+    }
+  else
+    {
+      if (priv->stack)
+        {
+          GList *children, *l;
+          GtkWidget *search;
+          GtkWidget *empty;
+
+          search = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "internal-search");
+          empty = gtk_stack_get_child_by_name (GTK_STACK (priv->stack), "no-search-results");
+          children = gtk_container_get_children (GTK_CONTAINER (priv->stack));
+          for (l = children; l; l = l->next)
+            {
+              GtkWidget *child = l->data;
+
+              if (include_internal ||
+                  (child != search && child != empty))
+                callback (child, callback_data);
+            }
+          g_list_free (children);
+        }
+    }
+}
+
+static void
+ide_shortcuts_window_set_view_name (IdeShortcutsWindow *self,
+                                    const gchar        *view_name)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GList *sections, *l;
+
+  g_free (priv->view_name);
+  priv->view_name = g_strdup (view_name);
+
+  sections = gtk_container_get_children (GTK_CONTAINER (priv->stack));
+  for (l = sections; l; l = l->next)
+    {
+      IdeShortcutsSection *section = l->data;
+
+      if (IDE_IS_SHORTCUTS_SECTION (section))
+        g_object_set (section, "view-name", priv->view_name, NULL);
+    }
+  g_list_free (sections);
+}
+
+static void
+ide_shortcuts_window_set_section_name (IdeShortcutsWindow *self,
+                                       const gchar        *section_name)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkWidget *section = NULL;
+
+  g_free (priv->initial_section);
+  priv->initial_section = g_strdup (section_name);
+
+  if (section_name)
+    section = gtk_stack_get_child_by_name (priv->stack, section_name);
+  if (section)
+    gtk_stack_set_visible_child (priv->stack, section);
+}
+
+static void
+update_accels_cb (GtkWidget *widget,
+                  gpointer   data)
+{
+  IdeShortcutsWindow *self = data;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (IDE_IS_SHORTCUTS_SHORTCUT (widget))
+    ide_shortcuts_shortcut_update_accel (IDE_SHORTCUTS_SHORTCUT (widget), priv->window);
+  else if (GTK_IS_CONTAINER (widget))
+    gtk_container_foreach (GTK_CONTAINER (widget), update_accels_cb, self);
+}
+
+static void
+update_accels_for_actions (IdeShortcutsWindow *self)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (priv->window)
+    gtk_container_forall (GTK_CONTAINER (self), update_accels_cb, self);
+}
+
+static void
+keys_changed_handler (GtkWindow          *window,
+                      IdeShortcutsWindow *self)
+{
+  update_accels_for_actions (self);
+}
+
+void
+ide_shortcuts_window_set_window (IdeShortcutsWindow *self,
+                                 GtkWindow          *window)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (priv->keys_changed_id)
+    {
+      g_signal_handler_disconnect (priv->window, priv->keys_changed_id);
+      priv->keys_changed_id = 0;
+    }
+
+  priv->window = window;
+
+  if (priv->window)
+    priv->keys_changed_id = g_signal_connect (window, "keys-changed",
+                                              G_CALLBACK (keys_changed_handler),
+                                              self);
+
+  update_accels_for_actions (self);
+}
+
+static void
+ide_shortcuts_window__list_box__row_activated (IdeShortcutsWindow *self,
+                                               GtkListBoxRow      *row,
+                                               GtkListBox         *list_box)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkWidget *section;
+
+  section = g_object_get_data (G_OBJECT (row), "gtk-shortcuts-section");
+  gtk_stack_set_visible_child (priv->stack, section);
+  gtk_popover_popdown (priv->popover);
+}
+
+static gboolean
+hidden_by_direction (GtkWidget *widget)
+{
+  if (IDE_IS_SHORTCUTS_SHORTCUT (widget))
+    {
+      GtkTextDirection dir;
+
+      g_object_get (widget, "direction", &dir, NULL);
+      if (dir != GTK_TEXT_DIR_NONE &&
+          dir != gtk_widget_get_direction (widget))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_shortcuts_window__entry__changed (IdeShortcutsWindow *self,
+                                     GtkSearchEntry      *search_entry)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  gchar *downcase = NULL;
+  GHashTableIter iter;
+  const gchar *text;
+  const gchar *last_section_name;
+  gpointer key;
+  gpointer value;
+  gboolean has_result;
+
+  text = gtk_entry_get_text (GTK_ENTRY (search_entry));
+
+  if (!text || !*text)
+    {
+      if (priv->last_section_name != NULL)
+        {
+          gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name);
+          return;
+
+        }
+    }
+
+  last_section_name = gtk_stack_get_visible_child_name (priv->stack);
+
+  if (g_strcmp0 (last_section_name, "internal-search") != 0 &&
+      g_strcmp0 (last_section_name, "no-search-results") != 0)
+    {
+      g_free (priv->last_section_name);
+      priv->last_section_name = g_strdup (last_section_name);
+    }
+
+  downcase = g_utf8_strdown (text, -1);
+
+  g_hash_table_iter_init (&iter, priv->keywords);
+
+  has_result = FALSE;
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      GtkWidget *widget = key;
+      const gchar *keywords = value;
+      gboolean match;
+
+      if (hidden_by_direction (widget))
+        match = FALSE;
+      else
+        match = strstr (keywords, downcase) != NULL;
+
+      gtk_widget_set_visible (widget, match);
+      has_result |= match;
+    }
+
+  g_free (downcase);
+
+  if (has_result)
+    gtk_stack_set_visible_child_name (priv->stack, "internal-search");
+  else
+    gtk_stack_set_visible_child_name (priv->stack, "no-search-results");
+}
+
+static void
+ide_shortcuts_window__search_mode__changed (IdeShortcutsWindow *self)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  if (!gtk_search_bar_get_search_mode (priv->search_bar))
+    {
+      if (priv->last_section_name != NULL)
+        gtk_stack_set_visible_child_name (priv->stack, priv->last_section_name);
+    }
+}
+
+static void
+ide_shortcuts_window_close (IdeShortcutsWindow *self)
+{
+  gtk_window_close (GTK_WINDOW (self));
+}
+
+static void
+ide_shortcuts_window_search (IdeShortcutsWindow *self)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  gtk_search_bar_set_search_mode (priv->search_bar, TRUE);
+}
+
+static void
+ide_shortcuts_window_constructed (GObject *object)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)object;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  G_OBJECT_CLASS (ide_shortcuts_window_parent_class)->constructed (object);
+
+  if (priv->initial_section != NULL)
+    gtk_stack_set_visible_child_name (priv->stack, priv->initial_section);
+}
+
+static void
+ide_shortcuts_window_finalize (GObject *object)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)object;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  g_clear_pointer (&priv->keywords, g_hash_table_unref);
+  g_clear_pointer (&priv->initial_section, g_free);
+  g_clear_pointer (&priv->view_name, g_free);
+  g_clear_pointer (&priv->last_section_name, g_free);
+  g_clear_pointer (&priv->search_items_hash, g_hash_table_unref);
+
+  g_clear_object (&priv->search_image_group);
+  g_clear_object (&priv->search_text_group);
+
+  G_OBJECT_CLASS (ide_shortcuts_window_parent_class)->finalize (object);
+}
+
+static void
+ide_shortcuts_window_dispose (GObject *object)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)object;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  g_signal_handlers_disconnect_by_func (priv->stack, G_CALLBACK (update_title_stack), self);
+
+  ide_shortcuts_window_set_window (self, NULL);
+
+  if (priv->header_bar)
+    {
+      gtk_widget_destroy (GTK_WIDGET (priv->header_bar));
+      priv->header_bar = NULL;
+      priv->popover = NULL;
+    }
+
+  G_OBJECT_CLASS (ide_shortcuts_window_parent_class)->dispose (object);
+
+#if 0
+  if (priv->main_box)
+    {
+      gtk_widget_destroy (GTK_WIDGET (priv->main_box));
+      priv->main_box = NULL;
+    }
+#endif
+}
+
+static void
+ide_shortcuts_window_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)object;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_SECTION_NAME:
+      {
+        GtkWidget *child = gtk_stack_get_visible_child (priv->stack);
+
+        if (child != NULL)
+          {
+            gchar *name = NULL;
+
+            gtk_container_child_get (GTK_CONTAINER (priv->stack), child,
+                                     "name", &name,
+                                     NULL);
+            g_value_take_string (value, name);
+          }
+      }
+      break;
+
+    case PROP_VIEW_NAME:
+      g_value_set_string (value, priv->view_name);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_window_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)object;
+
+  switch (prop_id)
+    {
+    case PROP_SECTION_NAME:
+      ide_shortcuts_window_set_section_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_VIEW_NAME:
+      ide_shortcuts_window_set_view_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcuts_window_unmap (GtkWidget *widget)
+{
+  IdeShortcutsWindow *self = (IdeShortcutsWindow *)widget;
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  gtk_search_bar_set_search_mode (priv->search_bar, FALSE);
+
+  GTK_WIDGET_CLASS (ide_shortcuts_window_parent_class)->unmap (widget);
+}
+
+static GType
+ide_shortcuts_window_child_type (GtkContainer *container)
+{
+  return GTK_TYPE_SHORTCUTS_SECTION;
+}
+
+static void
+ide_shortcuts_window_class_init (IdeShortcutsWindowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  GtkBindingSet *binding_set = gtk_binding_set_by_class (klass);
+
+  object_class->constructed = ide_shortcuts_window_constructed;
+  object_class->finalize = ide_shortcuts_window_finalize;
+  object_class->get_property = ide_shortcuts_window_get_property;
+  object_class->set_property = ide_shortcuts_window_set_property;
+  object_class->dispose = ide_shortcuts_window_dispose;
+
+  widget_class->unmap = ide_shortcuts_window_unmap;
+  container_class->add = ide_shortcuts_window_add;
+  container_class->remove = ide_shortcuts_window_remove;
+  container_class->child_type = ide_shortcuts_window_child_type;
+  container_class->forall = ide_shortcuts_window_forall;
+
+  klass->close = ide_shortcuts_window_close;
+  klass->search = ide_shortcuts_window_search;
+
+  /**
+   * IdeShortcutsWindow:section-name:
+   *
+   * The name of the section to show.
+   *
+   * This should be the section-name of one of the #IdeShortcutsSection
+   * objects that are in this shortcuts window.
+   */
+  properties[PROP_SECTION_NAME] =
+    g_param_spec_string ("section-name", _("Section Name"), _("Section Name"),
+                         "internal-search",
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeShortcutsWindow:view-name:
+   *
+   * The view name by which to filter the contents.
+   *
+   * This should correspond to the #IdeShortcutsGroup:view property of some of
+   * the #IdeShortcutsGroup objects that are inside this shortcuts window.
+   *
+   * Set this to %NULL to show all groups.
+   */
+  properties[PROP_VIEW_NAME] =
+    g_param_spec_string ("view-name", _("View Name"), _("View Name"),
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  /**
+   * IdeShortcutsWindow::close:
+   *
+   * The ::close signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted when the user uses a keybinding to close
+   * the window.
+   *
+   * The default binding for this signal is the Escape key.
+   */
+  signals[CLOSE] = g_signal_new (_("close"),
+                                 G_TYPE_FROM_CLASS (klass),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                 G_STRUCT_OFFSET (IdeShortcutsWindowClass, close),
+                                 NULL, NULL, NULL,
+                                 G_TYPE_NONE,
+                                 0);
+
+  /**
+   * IdeShortcutsWindow::search:
+   *
+   * The ::search signal is a
+   * [keybinding signal][GtkBindingSignal]
+   * which gets emitted when the user uses a keybinding to start a search.
+   *
+   * The default binding for this signal is Control-F.
+   */
+  signals[SEARCH] = g_signal_new (_("search"),
+                                 G_TYPE_FROM_CLASS (klass),
+                                 G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                 G_STRUCT_OFFSET (IdeShortcutsWindowClass, search),
+                                 NULL, NULL, NULL,
+                                 G_TYPE_NONE,
+                                 0);
+
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "close", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_f, GDK_CONTROL_MASK, "search", 0);
+
+  g_type_ensure (GTK_TYPE_SHORTCUTS_GROUP);
+  g_type_ensure (GTK_TYPE_SHORTCUTS_SHORTCUT);
+}
+
+static gboolean
+window_key_press_event_cb (GtkWidget *window,
+                           GdkEvent  *event,
+                           gpointer   data)
+{
+  IdeShortcutsWindow *self = IDE_SHORTCUTS_WINDOW (window);
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+
+  return gtk_search_bar_handle_event (priv->search_bar, event);
+}
+
+static void
+ide_shortcuts_window_init (IdeShortcutsWindow *self)
+{
+  IdeShortcutsWindowPrivate *priv = ide_shortcuts_window_get_instance_private (self);
+  GtkToggleButton *search_button;
+  GtkBox *menu_box;
+  GtkBox *box;
+  GtkArrow *arrow;
+  GtkWidget *scroller;
+  GtkWidget *label;
+  GtkWidget *empty;
+  PangoAttrList *attributes;
+
+  gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+  gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_DIALOG);
+
+  g_signal_connect (self, "key-press-event",
+                    G_CALLBACK (window_key_press_event_cb), NULL);
+
+  priv->keywords = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+  priv->search_items_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  priv->search_text_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+  priv->search_image_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+
+  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_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (search_button)), "image-button");
+  gtk_container_add (GTK_CONTAINER (priv->header_bar), GTK_WIDGET (search_button));
+
+  priv->main_box = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_VERTICAL,
+                           "visible", TRUE,
+                           NULL);
+  GTK_CONTAINER_CLASS (ide_shortcuts_window_parent_class)->add (GTK_CONTAINER (self), GTK_WIDGET 
(priv->main_box));
+
+  priv->search_bar = g_object_new (GTK_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 (priv->main_box), GTK_WIDGET (priv->search_bar));
+
+  priv->stack = g_object_new (GTK_TYPE_STACK,
+                              "expand", TRUE,
+                              "homogeneous", TRUE,
+                              "transition-type", GTK_STACK_TRANSITION_TYPE_CROSSFADE,
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (priv->main_box), GTK_WIDGET (priv->stack));
+
+  priv->title_stack = g_object_new (GTK_TYPE_STACK,
+                                    "visible", TRUE,
+                                    NULL);
+  gtk_header_bar_set_custom_title (priv->header_bar, GTK_WIDGET (priv->title_stack));
+
+  label = gtk_label_new (_("Shortcuts"));
+  gtk_widget_show (label);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE);
+  gtk_stack_add_named (priv->title_stack, label, "title");
+
+  label = gtk_label_new (_("Search Results"));
+  gtk_widget_show (label);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_TITLE);
+  gtk_stack_add_named (priv->title_stack, label, "search");
+
+  priv->menu_button = g_object_new (GTK_TYPE_MENU_BUTTON,
+                                    "focus-on-click", FALSE,
+                                    "visible", TRUE,
+                                    "relief", GTK_RELIEF_NONE,
+                                    NULL);
+  gtk_stack_add_named (priv->title_stack, GTK_WIDGET (priv->menu_button), "sections");
+
+  menu_box = g_object_new (GTK_TYPE_BOX,
+                           "orientation", GTK_ORIENTATION_HORIZONTAL,
+                           "spacing", 6,
+                           "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));
+
+  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 (ide_shortcuts_window__list_box__row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add (GTK_CONTAINER (priv->popover), GTK_WIDGET (priv->list_box));
+
+  priv->search_entry = GTK_SEARCH_ENTRY (gtk_search_entry_new ());
+  gtk_widget_show (GTK_WIDGET (priv->search_entry));
+  gtk_container_add (GTK_CONTAINER (priv->search_bar), GTK_WIDGET (priv->search_entry));
+  g_object_set (priv->search_entry,
+                "placeholder-text", _("Search Shortcuts"),
+                "width-chars", 40,
+                NULL);
+  g_signal_connect_object (priv->search_entry,
+                           "search-changed",
+                           G_CALLBACK (ide_shortcuts_window__entry__changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (priv->search_bar,
+                           "notify::search-mode-enabled",
+                           G_CALLBACK (ide_shortcuts_window__search_mode__changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                           "visible", TRUE,
+                           NULL);
+  box = g_object_new (GTK_TYPE_BOX,
+                      "border-width", 24,
+                      "halign", GTK_ALIGN_CENTER,
+                      "spacing", 24,
+                      "orientation", GTK_ORIENTATION_VERTICAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (box));
+  gtk_stack_add_named (priv->stack, scroller, "internal-search");
+
+  priv->search_shortcuts = g_object_new (GTK_TYPE_BOX,
+                                         "halign", GTK_ALIGN_CENTER,
+                                         "spacing", 6,
+                                         "orientation", GTK_ORIENTATION_VERTICAL,
+                                         "visible", TRUE,
+                                         NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_shortcuts));
+
+  priv->search_gestures = g_object_new (GTK_TYPE_BOX,
+                                        "halign", GTK_ALIGN_CENTER,
+                                        "spacing", 6,
+                                        "orientation", GTK_ORIENTATION_VERTICAL,
+                                        "visible", TRUE,
+                                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (priv->search_gestures));
+
+  empty = g_object_new (GTK_TYPE_GRID,
+                        "visible", TRUE,
+                        "row-spacing", 12,
+                        "margin", 12,
+                        "hexpand", TRUE,
+                        "vexpand", TRUE,
+                        "halign", GTK_ALIGN_CENTER,
+                        "valign", GTK_ALIGN_CENTER,
+                        NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (empty), GTK_STYLE_CLASS_DIM_LABEL);
+  gtk_grid_attach (GTK_GRID (empty),
+                   g_object_new (GTK_TYPE_IMAGE,
+                                 "visible", TRUE,
+                                 "icon-name", "edit-find-symbolic",
+                                 "pixel-size", 72,
+                                 NULL),
+                   0, 0, 1, 1);
+  attributes = pango_attr_list_new ();
+  pango_attr_list_insert (attributes, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+  pango_attr_list_insert (attributes, pango_attr_scale_new (1.44));
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "visible", TRUE,
+                        "label", _("No Results Found"),
+                        "attributes", attributes,
+                        NULL);
+  pango_attr_list_unref (attributes);
+  gtk_grid_attach (GTK_GRID (empty), label, 0, 1, 1, 1);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "visible", TRUE,
+                        "label", _("Try a different search"),
+                        NULL);
+  gtk_grid_attach (GTK_GRID (empty), label, 0, 2, 1, 1);
+
+  gtk_stack_add_named (priv->stack, empty, "no-search-results");
+
+  g_signal_connect_object (priv->stack, "notify::visible-child",
+                           G_CALLBACK (update_title_stack), self, G_CONNECT_SWAPPED);
+
+}
diff --git a/libide/shortcuts/ide-shortcuts-window.h b/libide/shortcuts/ide-shortcuts-window.h
new file mode 100644
index 0000000..98df8ac
--- /dev/null
+++ b/libide/shortcuts/ide-shortcuts-window.h
@@ -0,0 +1,57 @@
+/* ide-shortcuts-window.h
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Library General Public License as
+ *  published by the Free Software Foundation; either version 2 of the
+ *  License, or (at your option) any later version.
+ *
+ *  This library 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
+ *  Library General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Library General Public
+ *  License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __IDE_SHORTCUTS_WINDOW_H__
+#define __IDE_SHORTCUTS_WINDOW_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_WINDOW            (ide_shortcuts_window_get_type ())
+#define IDE_SHORTCUTS_WINDOW(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_SHORTCUTS_WINDOW, 
IdeShortcutsWindow))
+#define IDE_SHORTCUTS_WINDOW_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_SHORTCUTS_WINDOW, 
IdeShortcutsWindowClass))
+#define IDE_IS_SHORTCUTS_WINDOW(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IDE_TYPE_SHORTCUTS_WINDOW))
+#define IDE_IS_SHORTCUTS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_SHORTCUTS_WINDOW))
+#define IDE_SHORTCUTS_WINDOW_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_SHORTCUTS_WINDOW, 
IdeShortcutsWindowClass))
+
+
+typedef struct _IdeShortcutsWindow         IdeShortcutsWindow;
+typedef struct _IdeShortcutsWindowClass    IdeShortcutsWindowClass;
+
+
+struct _IdeShortcutsWindow
+{
+  GtkWindow window;
+};
+
+struct _IdeShortcutsWindowClass
+{
+  GtkWindowClass parent_class;
+
+  void (*close)  (IdeShortcutsWindow *self);
+  void (*search) (IdeShortcutsWindow *self);
+};
+
+GType ide_shortcuts_window_get_type (void) G_GNUC_CONST;
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(IdeShortcutsWindow, g_object_unref)
+
+G_END_DECLS
+
+#endif /* IDE_SHORTCUTS_WINDOW _H */



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