Skip to content

Inputs and navigation

Nathan S edited this page Jun 16, 2020 · 6 revisions

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.

Navigation and focus

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 return this. Returning nullptr means that neither the view nor any of its children are focusable.
    • For instance, BoxLayout itself is not focusable but its children may be - in BoxLayout::getDefaultFocus(), we return the default focus of the first focusable child view, or nullptr 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.
  • getNextFocus(): that returns the next child view to focus on a given direction. Returning nullptr 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 return nullptr 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 call getDefaultFocus() 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

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.

Actions and hints

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.

Clone this wiki locally