Skip to content

Keyboard Navigation

Peter Jonas edited this page Feb 2, 2024 · 1 revision

Keyboard Navigation

General Keyboard accessibility rule
All functionality should be accessible via the keyboard

Sections, panels, controls

Sections and panels (subsections) are logical areas into which we have separated the application. Controls are controls such as buttons, menus, checkbox and etc.

navigation_sections_and_panels.png

Workflow

Navigation is performed using the keyboard, specific keys, or key sequences. The navigation system does not work directly with keyboard events, it works with actions. And we can assign shortcuts to the appropriate actions to send them. This allows us not to bind to specific keys, allows us to reassign shortcuts, for example, for up, left, down, right to use the WASD keys.

navigation_workflow.png

Navigation Controller

The navigation controller contains the main logic of the navigation, it determines which section, panel, control needs to be made active when receiving actions. To do this, it subscribes to receive the corresponding actions

void NavigationController::init()
{
    dispatcher()->reg(this, "nav-next-section", this, &NavigationController::goToNextSection);
    dispatcher()->reg(this, "nav-prev-section", [this]() { goToPrevSection(false); });
    dispatcher()->reg(this, "nav-next-panel", this, &NavigationController::goToNextPanel);
    dispatcher()->reg(this, "nav-prev-panel", this, &NavigationController::goToPrevPanel);
    dispatcher()->reg(this, "nav-right", this, &NavigationController::onRight);
    dispatcher()->reg(this, "nav-left", this, &NavigationController::onLeft);
    dispatcher()->reg(this, "nav-up", this, &NavigationController::onUp);
    dispatcher()->reg(this, "nav-down", this, &NavigationController::onDown);
    dispatcher()->reg(this, "nav-escape", this, &NavigationController::onEscape);
    ...
}

Navigation Actions

To be able to send a navigation action, we need to make a UI Action for it (as well as for any other actions that the user can initiate using shortcuts or UI controls) We don't have to give them a title or icon, because we will only send them using shortcuts, no menu items or buttons (and we could)

navigationuiactions.cpp

const UiActionList NavigationUiActions::m_actions = {
    UiAction("nav-next-section",
             mu::context::UiCtxAny
             ),
    UiAction("nav-prev-section",
             mu::context::UiCtxAny
             ),
    UiAction("nav-next-panel",
             mu::context::UiCtxAny
             ),
    UiAction("nav-prev-panel",
             mu::context::UiCtxAny
             ),
    UiAction("nav-next-tab",
             mu::context::UiCtxAny
             ),
    UiAction("nav-prev-tab",
             mu::context::UiCtxAny
             ),
    UiAction("nav-right",
             mu::context::UiCtxAny
             ),
    UiAction("nav-left",
             mu::context::UiCtxAny
             ),
    ...

Navigation Events

Before making a section, panel and controller active, the navigation controller sends them events that they can process. This allows us to make custom logic and interrupt the default logic if set an accepted flag for the event. The controller also sends events for control actions, for example, when pressing Esc.

SomePopup.qml

    NavigationPanel {
        id: navPanel
        
        ...

        onNavigationEvent: {
            if (event.type === NavigationEvent.Escape) {
                root.close()
            }
        }
    }

Navigation Control

Navigation control is just a control object, it does not contain any visual component, it has properties and signals for control. Typical use case

SampleButton.qml

import QtQuick 2.15
import MuseScore.Ui 1.0

FocusScope {
    id: root

    property alias text: label.text

    property alias navigation: navCtrl

    signal clicked()

    function ensureActiveFocus() {
        if (!root.activeFocus) {
            root.forceActiveFocus()
        }

        if (!navCtrl.active) {
            navCtrl.forceActive()
        }
    }

    NavigationControl {
        id: navCtrl
        name: root.objectName != "" ? root.objectName : "SampleButton"
        enabled: root.enabled && root.visible
        onActiveChanged: {
            if (!root.activeFocus) {
                root.forceActiveFocus()
            }
        }
        onTriggered: root.clicked()
    }

    Rectangle {
        id: backgroud
        anchors.fill: parent

        color: ui.theme.buttonColor

        border.width: navCtrl.active ? 2 : 0
        border.color: ui.theme.focusColor
    }

    Text {
        id: label
        ...
    }

    MouseArea {

        anchors.fill: parent

        onClicked: {
            root.ensureActiveFocus()
            root.clicked()
        }
    }
}

Navigation Index

To set the desired traversal order for the elements, we must set the order for the section and panels and the index for the controls.

Panels can have a traversal direction:

  • Horizontal - moving between controls using the right - left keys. The controllers must have an index.column.
  • Vertical - moving between controls using the up and down keys. Controllers must have an index.row
  • Both - move in all directions, like a table. Controls must have index.row and index.column

It can be difficult to arrange the correct orders and indexes. There is a tool that can help you with this. see Dev Tools

Example:

NavigationSection {
    id: navTopSection

    name: "TopSection"
    order: 1
}

NavigationPanel {
    id: navTopPanel1

    name: "TopPanel1"
    section: navTopSection
    order: 1
    direction: NavigationPanel.Vertical
}

NavigationControl {
    id: navTopPanel1_Control1

    name: "TopPanel1_Control1"
    panel: navTopPanel1
    index.row: 1
} 

NavigationControl {
    id: navTopPanel1_Control2

    name: "TopPanel1_Control2"
    panel: navTopPanel1
    index.row: 2
}   

NavigationPanel {
    id: navTopPanel2

    name: "TopPanel2"
    section: navTopSection
    order: 2
}
...


NavigationSection {
    id: navLeftSection

    name: "LeftSection"
    order: 2
}

...

Dev Tools

You can press Ctrl+F1 and a dialog will open with all sections, panels, and controls that are currently registered. In this dialog, you can see what state the elements are in and what their order (index) is.

Testing

Translation

Compilation

  1. Set up developer environment
  2. Install Qt and Qt Creator
  3. Get MuseScore's source code
  4. Install dependencies
  5. Compile on the command line
  6. Compile in Qt Creator

Beyond compiling

  1. Find your way around the code
  2. Submit a Pull Request
  3. Fix the CI checks

Misc. development

Architecture general

Audio

Engraving

Extensions

Google Summer of Code

References

Clone this wiki locally