Notes on Actions and Keybindings in Ardour
Actions and Bindings are the basic building blocks of how a user interacts with Ardour via a keyboard or in many cases a control surface.
Anything a user can initiate via a solitary interaction that does not contain any geometric information should be represented as an Action. This does not cover mouse/touch events, since these necessarily contain geometric information. It does cover all keyboard events, as well as many types of interactions with various control surfaces, since they will also not send geometric information but instead just a request to "do something".
This description of "everything is an Action" is an ideal to which our codebas aspires. At the time of writing, we don't get there. Nevertheless, everything you can find in any menu anywhere in Ardour is actually an Action, and there are many Actions not used in menus.
Why is geometric information relevant ?
If an input event from a user contains geometric information, it implies that the location of the event in the user interface may be (and likely is) important. The user wants to operate on a particular object or a particular point on the timeline. Actions use closures, which are objects that are be invoked like the simplest traditional C function, without any additional arguments. There is therefore no way to pass information about the target of the action to the closure. Actions must operate on a predefined target (though this can include "the current selection", which opens up many possibilities).
What is an Action?
An action is an object that consists of 3 critical elements:
- a path, which uniquely defines the action. Syntatically it looks like a Unix filesystem path ("/path/to/somewhere"), but has no relationship to any filesystem. At the time of writing, most action paths in ardour consist of just two elements, the first of which is the name of the group the action belongs to, and the second being its name.
- a name, which will appear in the user interface in various contexts (e.g. menus, button labels)
- a closure (or functor in C++ terminology) that will be invoked when the Action is activated. The closure is where stuff "actually happens", and there are no limits or restrictions on what it might actually do. We generally use sigc::mem_fun to build closures used with Actions.
Sub-classes of Action include ToggleAction (which adds the concept of the Action being active or inactive) and RadioAction (which allows for sets of ToggleActions, but with only 1 active at any time).
Given a defined Action, it may be used in a variety of ways.
- In menus. These are defined using GTK's UIManager syntax, which oddly strips all but the last component of the path when specifying Actions. This is potentially problematic if the last component is re-used within different groups of actions. Most of Ardour's menus are defined this way (see, for example, gtk2_ardour/ardour.menus.in).
- As "related actions" for various widgets. This can be done implicitly, by asking the GTK UIManager to supply a widget for a given Action. It can also be done explicitly by calling ::set_related_action() on various types of widgets (notably Buttons). When the widget is "activated", the Action will be activated too, and for ToggleActions, when the widget becomes "active", the Action will do so as well.
- Explicitly used. If you have a reference to an Action, you can operate on it directly, by calling ::activate() or (for ToggleActions) ::set_active(). When this is done, all widgets that are related to the Action will change their visual state if appropriate.
Actions are always created as part of an ActionGroup. ActionGroups are simply named collections of Actions. The name of the group forms part of the path for the actions in the group.
Glib::RefPtrThe above code creates an action group called "GroupName", and an action that belongs to the group. The action's path is "/GroupName/foobar", and it's visible name is "Foo Bar".
group = ActionManager::create_action_group (bindings, X_("GroupName")); Glib::RefPtr action = ActionManager::register_action (group, "foobar", "Foo Bar", ...);
In Ardour we also require that a set of Bindings is given as the nominal owner of the Actions in the ActionGroup. This does not limit the Actions to only being used by a particular set of Bindings, but it helps with visual and logical organization in the key binding editor, since the list of actions associated with the a given set of Bindings will be restricted to those in groups owned by that set of Bindings.
Actions and ActionGroups are all created using an API that is inside the ActionManager namespace. ActionManager is not an object, because we wanted/needed to allow it to be extended in ways that don't really make sense for an object. Inside the gtk2_ardour/ folder, there is a chunk of ActionManager API that adds to what is provided to all UIs (control surfaces etc.), specifically for the GUI.
Note that this means that all Actions and ActionGroups are globally accessible.
Interactions with GTK Menus
GTK offers a nice feature: if a menu is built out of MenuItems associated with Actions, and if there are GTK "accelerators" associated with the Action, it will automatically display the accelerator (key binding) right-justified in the menu.
We do not want to reimplement this, but for it to work, we have to
make sure that our own keyboard bindings (see below) are also
represented as GTK's accelerators. This done via a bit of magic in the
Bindings code (
Bindings::push_to_gtk()), where a comment
explains some of the unfortunate complexities.
In Ardour, we handle keyboard interactions (and also in many cases, button presses on control surfaces) as up to two distinct events: a press, and a release.
A given key event consists of a code indicating the key that was pressed (or released), and a set of modifiers (Control, Command, Alt etc.) that were in effect at the time of the event.
A single Binding consists of a key event, the name of an action, and a reference to the action (if found).
A Bindings object is just a named collection of Binding objects. A
Bindings object may be associated with any
in Ardour (notably, any widget) using this sort of thing:
Bindings* bindings = .... object->set_data (X_("ardour-bindings"), bindings);
Key Event Handling
Ardour more or less completely takes over key event handling from GTK. In GTK, key events are delivered to the top level window widget in which they occur, so that it may handle it appropriately given the contents of the window (focus handling policy etc. etc.).
Ardour intervenes at that level, receiving the key event before it is given to the Gtk::Window widget, and returning as if the event was fully handled. Every top level window has ARDOUR_UI::key_event_handler() connected as the first handler for key press and key release events.
Key events are delivered to the window in which they happen. Ardour will determine if there is a widget in the window which currently has "focus" (indicating that all keyboard events for that window should be delivered to it). It will further determine if the widget with focus appears to be used for text entry. If so, then Ardour will do no additional processing of keyboard events - they will just be passed directly to the focus widget, so that they can be used for inputting text (with all the complexities that implies).
If there is a focus widget and it does not appear to be used for text
entry, then Ardour will use
determine if there is a specific set of bindings associated with the
widget. If there is, then these bindings will "activated" by the key
event. If this activation returns true, no further processing of the
key event will occur (it will be considered "handled"). If the return
value is false, then we will continue as if there was no focus widget.
If there is no focus widget, then Ardour will search for a set of bindings associated with the widget that received the event from GTK. If none are found, it will check the parent of that widget, and so on until the top level window is found. If there are still no bindings found, then a set of global bindings will be "activated" using the key event.
Activating a binding set is a simple process. The binding set just looks to see if it contains the incoming key event (e.g. "s was pressed with the control modifier active"), and if it does, then the action associated with the key event will be activated.
If no binding set (even the global one) handles the key event, we do hand over the event back to GTK to do its own processing with. This allows us to avoid defining our own actions for common GUI operations such as tab-ing between fields and moving up and down in lists and menus.
At the time of writing, the behaviour of control surfaces is handled
with one of two mechanisms. When a control on a control surface is
intended to have precisely the same smeantics as some equivalent
button in the GUI, the easiest way to make this happen.is to use
BasicUI::access_action(), which takes the path of an action and
attempts to cause it to be activated. For more sophisticated
functionality, control surfaces have their own code to maintain their
own state and change the current state of various parts of the Session