-
Notifications
You must be signed in to change notification settings - Fork 87
Inputs and navigation
The strength of borealis resides in its layout and navigation system. More particularly, the focus order (which view to go to next when you press a key) is automatically detected from your app layout. As long as you use built-in layouts and focusable views, the navigation keys will always work out of the box. You can of course implement your own navigation order when building custom layouts.
There is also an integrated actions and hints system - you can bind any action to any key on any view, and they will automatically appear on the bottom-right hints ("B Back A OK") when the view (or one of its children) is focused.
The secret in the navigation system lies in the layout system. If you know the views layout, you know exactly what view to focus when pressing any key.
For instance in a horizontal BoxLayout
, pressing right will focus the next view of the layout. Pressing left will focus the previous one. Pressing up or down will look for the parent view and see if they have any view to focus next in that direction.
This lookup is done using two methods in the View
class:
-
getDefaultFocus()
: that returns the view to focus when trying to focus a view. It can be either the view itself or any of its children. To make a view focusable, simply returnthis
. Returningnullptr
means that neither the view nor any of its children are focusable.- For instance,
BoxLayout
itself is not focusable but its children may be - inBoxLayout::getDefaultFocus()
, we return the default focus of the first focusable child view, ornullptr
if none are found - Calls to that methods are chained. For example in a simple tab-based app, the default focus of the main frame is the tab view -> first tab -> right pane of the tab -> items list -> first item.
- For instance,
-
getNextFocus()
: that returns the next child view to focus on a given direction. Returningnullptr
here means that no view is to be focused on that direction -getNextFocus()
will then automatically be called again on our parent, and so on until a view is found or the root of the tree is reached.- In
BoxLayout
we returnnullptr
if the direction doesn't match the one of the layout. Otherwise we return the first focusable view in the given direction, if any. -
getNextFocus()
must callgetDefaultFocus()
on the next view to focus before returning it, to make sure that it's focusable - Focus lookups should generally be made on the
getDefaultFocus()
result of each view instead of the view directly
- In
The full algorithm for focus lookup can be found in the Application::navigate()
method.
Implementing one or both of these methods is enough to implement navigation in any view, whether it's a tree leaf or a node.
A BoxLayout
doesn't know what the index of a view in its internal array is. Or more precisely, when we give it the currently focused view, it doesn't know what the next one is if it cannot know the index of the focused one.
This is why each view has what we call "parent user data", to translate between the currently focused view pointer (View*
) and the internal layout structure (layout-specific data). The parent layout is fully responsible for allocating and writing to that field (or not if there is no need to).
When getNextFocus()
is called, it is always given the parent user data of one of its direct children.
For instance in such a scenario:
- A: Horizontal
BoxLayout
- Anything
- B: Vertical
BoxLayout
- List items
If a list item is focused and the left navigation key is pressed, getNextFocus()
will be called on B with the index of the currently focused list item. It will return nullptr
because the navigation direction doesn't match the layout direction. The method will then be called again on A, with the index of B and NOT the index of the list item (because B is the parent of the list item and the child of A). The A layout will know that the view on the left of B is Anything
, and will return getDefaultFocus()
of Anything
.
You can additionnally bind what we call "actions" to any view. An action consists of a name, a key and a callback. Actions can either be visible or hidden.
When the focus goes on a view, all of its actions and the ones of its parents are displayed in the top-bottom "hint" area. Hidden actions are not displayed here. When the user presses an action key, the corresponding callback code is executed.
The hint area is managed by Hint
, a view that you can use anywhere you want to display current focus actions. There can only be one Hint
on screen at any time (to match HOS behavior), so they are automatically hidden and shown as you push and pop views on the stack.
To bind an action to a view, simply call registerAction
on the view. You can bind any key but the navigation ones. You cannot bind multiple actions for the same button on the same view. You can use updateActionHint
to change the hint text of an already existing action.
Keep in mind that some views like Button
, ListItem
or PopupView
already bind actions for the A and/or B buttons. Most of them expose listeners to handle A button presses. This is a design choice, however questionable as actions should be user-provided for all views for consistency.