[gnome-builder/wip/chergert/merge-shortcuts] shortcuts: import shortcut engine
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/merge-shortcuts] shortcuts: import shortcut engine
- Date: Tue, 23 May 2017 02:13:31 +0000 (UTC)
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 (¶ms[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 (¶ms[1+i], G_VALUE_TYPE (src_value));
+ g_value_copy (src_value, ¶ms[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 (¶ms[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 (¶ms);
+
+ 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 <, > and & 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]