In previous posts, "Define UI comprising Dropdown in Blueprint for modern GTK apps" and "Define UI comprising ListView in Blueprint for modern GTK apps", I presented how to use list widgets in Blueprint. Now we look at a different, but more compact topic, the keyboard shortcut.
In a desktop app, the user is expected to do a lot of work with the keyboard alone, not always reaching for the mouse. So a good app should have many keyboard shortcuts. For example, a Ctrl+V to paste, a Ctrl+S to save, an F1 to show help, etc. Most GUI toolkits have built-in support for this. GTK is no exception. There are two ways to assign a keyboard shortcut to an action in GTK 4:
- The simplest one is the application's accelerator, set via
gtk_application_set_accels_for_action(). You define the action on the application (with theapp.prefix), then call that function with a list of accelerator strings. But this is not declarative, you have to write code. - A more flexible way is to use a
Gtk.ShortcutController, to which you add one or moreGtk.Shortcutobjects. EachShortcutbinds a trigger (a key combination) to an action. This is what we will learn in this post, because it is declarative and we can describe the shortcut in the Blueprint file alongside the rest of the UI.
This is the UI where the shortcut is used, in my CoBang app:

It is the image-input page of the scanner. User can drag and drop an image file, or use the Ctrl+V shortcut to paste an image from the clipboard. In the rest of this post, I will focus on the Ctrl+V shortcut part.
This is the Blueprint code, from scanner-page.blp file:
Frame frame_image {
focusable: true;
vexpand: true;
styles ['needs-highlight']
child: Box box_image_empty {
/* ... the empty-state UI of the image frame, omitted for brevity ... */
};
ShortcutController {
Shortcut {
trigger: '<Control>v';
action: 'action(win.paste-image)';
}
}
DropTargetAsync image_drop_target {
formats: 'GFile';
actions: copy;
accept => $on_image_drop_target_accept();
drop => $on_image_dropped();
}
}
A Shortcut is wrapped by a Gtk.ShortcutController, and the controller is attached to a widget. In CoBang, we attach the controller to the frame_image, the frame around the image preview area. That is the natural scope: only when this frame has focus, the Ctrl+V shortcut will be active.
Why a Frame and not the whole page? Because there are other widgets in the same page where Ctrl+V should not intercept paste. For example, the search entry, the dropdown, etc. By attaching the ShortcutController to the frame_image, the shortcut is only active when the user is in the image area, which is the expected behavior.
Now, let's look at the Shortcut itself:
Shortcut {
trigger: '<Control>v';
action: 'action(win.paste-image)';
}
It has two properties: trigger and action. Both are strings, parsed by GTK into Gtk.ShortcutTrigger and Gtk.ShortcutAction instances. We will go over them one by one.
The trigger property is described by the GTK docs as:
never, forGtkNeverTrigger.- A string parsed by
gtk_accelerator_parse(), for aGtkKeyvalTrigger, e.g.<Control>C. - Underscore, followed by a single character, for a
GtkMnemonicTrigger, e.g._l. - Two or more valid trigger strings, separated by a
|character, for aGtkAlternativeTrigger:<Control>q|<Control>w.
So our '<Control>v' is a GtkKeyvalTrigger, activated by pressing the v key while the Control modifier is held.
The action property is described by the GTK docs as:
nothing, forGtkNothingAction.activate, forGtkActivateAction.mnemonic-activate, forGtkMnemonicAction.action(NAME), for aGtkNamedActionfor the action namedNAME.signal(NAME), for aGtkSignalActionfor the signalNAME.
So our 'action(win.paste-image)' is a GtkNamedAction for the action named win.paste-image. The win. prefix is the action scope, which tells GTK where to look up the action. The win. prefix means the action should be looked up on the nearest enclosing Gtk.Window, which is CoBangWindow in our case.
In window.py, the paste-image action is registered like this:
action = Gio.SimpleAction.new('paste-image', None)
self.add_action(action)
action.connect('activate', self.on_paste_image)
Because we call self.add_action(...) on CoBangWindow (which is an Adw.ApplicationWindow), the action is registered on the window's action group, under the name paste-image. The win. prefix in the Blueprint file matches that.
In the future, I may also want to list the shortcut in a "Keyboard Shortcuts" dialog (the one you usually get with F1 or via the help menu), so that users can discover it without reading the documentation. Such a dialog is built with Adw.ShortcutsDialog and Adw.ShortcutsItem, but that is for a different post.
And that is how you add a keyboard shortcut in Blueprint, with just a few lines, and no application code needed. Hope this helps — feel free to reach out if you have questions! 🥰