--- title: Define UI comprising keyboard Shortcut in Blueprint for modern GTK apps date: 2026-07-22 11:39:18.589146 UTC --- In previous posts, ["Define UI comprising Dropdown in Blueprint for modern GTK apps"][dropdown-post] and ["Define UI comprising ListView in Blueprint for modern GTK apps"][listview-post], I presented how to use list widgets in [Blueprint][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()`][set-accels]. You define the action on the application (with the `app.` 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`][shortcut-controller], to which you add one or more [`Gtk.Shortcut`][shortcut] objects. Each `Shortcut` binds 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][cobang] app: ![Paste image with Ctrl+V](https://quan-images.b-cdn.net/blogs/2026/07/CoBang_v1_5.png) 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_](https://github.com/hongquan/CoBang/blob/5b10a94ad0e7cb07302d53e2257814e0577bf91e/src/ui/scanner-page.blp) file: ```blp 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: '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`][shortcut-controller], 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: ```blp Shortcut { trigger: 'v'; action: 'action(win.paste-image)'; } ``` It has two properties: `trigger` and `action`. Both are strings, parsed by GTK into [`Gtk.ShortcutTrigger`][shortcut-trigger] and [`Gtk.ShortcutAction`][shortcut-action] instances. We will go over them one by one. The `trigger` property is described by the [GTK docs][shortcut-trigger.parse-string] as: - `never`, for [`GtkNeverTrigger`][never-trigger]. - A string parsed by [`gtk_accelerator_parse()`][accel-parse], for a [`GtkKeyvalTrigger`][keyval-trigger], e.g. `C`. - Underscore, followed by a single character, for a [`GtkMnemonicTrigger`][mnemonic-trigger], e.g. `_l`. - Two or more valid trigger strings, separated by a `|` character, for a [`GtkAlternativeTrigger`][alt-trigger]: `q|w`. So our `'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][shortcut-action.parse-string] as: - `nothing`, for [`GtkNothingAction`][nothing-action]. - `activate`, for [`GtkActivateAction`][activate-action]. - `mnemonic-activate`, for [`GtkMnemonicAction`][mnemonic-action]. - `action(NAME)`, for a [`GtkNamedAction`][named-action] for the action named `NAME`. - `signal(NAME)`, for a [`GtkSignalAction`][signal-action] for the signal `NAME`. So our `'action(win.paste-image)'` is a `GtkNamedAction` for the action named `win.paste-image`. The `win.` prefix is the [action scope][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_](https://github.com/hongquan/CoBang/blob/5b10a94ad0e7cb07302d53e2257814e0577bf91e/src/window.py), the `paste-image` action is registered like this: ```py 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`][shortcuts-dialog] and [`Adw.ShortcutsItem`][shortcuts-item], 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! 🥰 [dropdown-post]: /post/2025/11/define-ui-comprising-dropdown-in-blueprint-for-modern-gtk-apps [listview-post]: /post/2025/12/define-ui-comprising-listview-in-blueprint-for-modern-gtk-apps [blueprint]: https://gnome.pages.gitlab.gnome.org/blueprint-compiler/ [cobang]: https://github.com/hongquan/CoBang [set-accels]: https://docs.gtk.org/gtk4/method.Application.set_accels_for_action.html [shortcut-controller]: https://docs.gtk.org/gtk4/class.ShortcutController.html [shortcut]: https://docs.gtk.org/gtk4/class.Shortcut.html [shortcut-trigger]: https://docs.gtk.org/gtk4/class.ShortcutTrigger.html [shortcut-action]: https://docs.gtk.org/gtk4/class.ShortcutAction.html [shortcut-trigger.parse-string]: https://docs.gtk.org/gtk4/ctor.ShortcutTrigger.parse_string.html [shortcut-action.parse-string]: https://docs.gtk.org/gtk4/ctor.ShortcutAction.parse_string.html [never-trigger]: https://docs.gtk.org/gtk4/class.NeverTrigger.html [accel-parse]: https://docs.gtk.org/gtk4/func.accelerator_parse.html [keyval-trigger]: https://docs.gtk.org/gtk4/class.KeyvalTrigger.html [mnemonic-trigger]: https://docs.gtk.org/gtk4/class.MnemonicTrigger.html [alt-trigger]: https://docs.gtk.org/gtk4/class.AlternativeTrigger.html [nothing-action]: https://docs.gtk.org/gtk4/class.NothingAction.html [activate-action]: https://docs.gtk.org/gtk4/class.ActivateAction.html [mnemonic-action]: https://docs.gtk.org/gtk4/class.MnemonicAction.html [named-action]: https://docs.gtk.org/gtk4/class.NamedAction.html [signal-action]: https://docs.gtk.org/gtk4/class.SignalAction.html [action-scope]: https://docs.gtk.org/gtk4/section-action-scope.html [shortcuts-dialog]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.4/class.ShortcutsDialog.html [shortcuts-item]: https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.4/class.ShortcutsItem.html