UI Actions - musescore/MuseScore GitHub Wiki

UI Actions

UiActions are actions that the user performs.
The main difference between Actions and UiActions is that Actions are like a command or method call, which may will call in the any time, of technically possible, while UiActions are a description of an action for the user (title, icon, type, etc.) and user interaction logic ( the availability of the action for the user at the moment, the check state of the action, etc.)

Declaration

Each module declares its own UiActions, the typical class name with declarations is ModuleUiActions (for example NotationsUiActions).
Example:

const UiActionList NotationUiActions::m_actions = {        
    UiAction("next-element",
             mu::context::UiCtxNotationOpened,
             mu::context::CTX_NOTATION_FOCUSED,
             TranslatableString("action", "Next element"),
             TranslatableString("action", "Select next element in score")
             ),
    UiAction("prev-element",
             mu::context::UiCtxNotationOpened,
             mu::context::CTX_NOTATION_FOCUSED,
             TranslatableString("action", "Previous element"),
             TranslatableString("action", "Select previous element in score")
             ),
    ...         
    UiAction("add-braces",
             mu::context::UiCtxNotationOpened,
             mu::context::CTX_NOTATION_OPENED,
             TranslatableString("action", "Add braces to element"),
             TranslatableString("action", "Add braces to element"),
             IconCode::Code::BRACE
             ),
    UiAction("add-parentheses",
             mu::context::UiCtxNotationOpened,
             mu::context::CTX_ANY,
             TranslatableString("action", "Add parentheses to element"),
             TranslatableString("action", "Add parentheses to element"),
             IconCode::Code::BRACKET_PARENTHESES
             ),
    ...         
    UiAction("note-input-steptime",
             mu::context::UiCtxNotationOpened,
             mu::context::CTX_NOTATION_OPENED,
             TranslatableString("action", "Default (step time)"),
             TranslatableString("action", "Note input: toggle ‘default (step-time)’ mode"),
             IconCode::Code::EDIT
             ),     

When declaring UiActions can be specified:

  • action code - action which will be dispatched on activation
  • uiCtx- UI Context for which action is available (see UI Context)
  • scCtx- Shortcut Context for the shortcut which is assigned to this action
  • title - usually used for tooltips title
  • description - usually used for tooltips and the text that is displayed in the preferences listing
  • icon (code) - usually used for menu items and toolbutton icon
  • checkable - usually used for menu items

UIActions Register

All UiActions must be registered in the UiActions Register. Through this register, clients getting access to UiActions and their state, so UiActions Register - this is global register for all UiActions.

Registration takes place in the module setup

...
static std::shared_ptr<NotationActionController> s_actionController = std::make_shared<NotationActionController>();
static std::shared_ptr<NotationUiActions> s_notationUiActions = std::make_shared<NotationUiActions>(s_actionController);
...

void NotationModule::resolveImports()
{
    auto ar = ioc()->resolve<IUiActionsRegister>(moduleName());
    if (ar) {
        ar->reg(s_notationUiActions);
    }
...
}

void NotationModule::onInit(const IApplication::RunMode&)
{
    ...
    s_actionController->init();
    s_notationUiActions->init();
    ...
}

To be able to register in a register, the class must inherit and implement the ui::IUiActionsModule interface.

class NotationUiActions : public ui::IUiActionsModule
{
public:

    const ui::UiActionList& actionsList() const override;

    bool actionEnabled(const ui::UiAction& act) const override;
    async::Channel<actions::ActionCodeList> actionEnabledChanged() const override;

    bool actionChecked(const ui::UiAction& act) const override;
    async::Channel<actions::ActionCodeList> actionCheckedChanged() const override;
}

Clients (for example, menu or toolbar models) can get UiAction from the register

const UiAction& action = uiactionsRegister()->action("select-workspace");

It is also possible to get the state of the UiAction and subscibe on states changes

const UiActionState& state = uiactionsRegister()->actionState("select-workspace");

uiactionsRegister()->actionStateChanged().onReceive(this, [this](const ActionCodeList& codes) {
    ...
});

UIAction State

Each UiAction action has a state:

  • enabled - the action is available to the user
  • checked - action checked (only for checkable actions)

The availability of an action for the user is determined by the technical ability to perform the action and the logical one.
For example, there is an action "insert measure", technically, the ability to perform the action depends on whether the notation element is currently selected (to know where to insert the measure).
And logically for users, it also depends on where the users are now and what they are doing (for example, on which screen). For example, if we are on the notation screen (and the current element is selected), then the action "insert measure" is available for the user (for example, from a menu item). If we switch to the Home screen, then technically it is still possible to insert measure (the notation is loaded, the element is selected, it is just not visible), but it is illogical for the user to insert measures while on the Home screen, so the action should not be available to the user (for example, from the menu item).
When the state of the UiAction changes, there must be a notification about the change so that the user controls can be updated (for example, menu items, buttons on the toolbar, etc.)

Since conditions of so many UiActions depend on the UI Context, the condition of the current UI Context is checked centrally in the UiActionsRegister, for this it uses UiContextResolver (see UI Context).

...
    if (!ctxResolver->match(currentCtx, action.context)) {
        state.enabled = false;
    }
...    

Other logical conditions (if they present) are checked in ModuleUiActions, for this UiActionsRegister calling the actionEnabled method. In this method also checks the technical conditions, usually with the ActionController (see Actions)

bool NotationUiActions::actionEnabled(const UiAction& act) const
{
    if (!m_controller->canReceiveAction(act.code)) { // check technical conditions
        return false;
    }

    // can be check some logic conditions, if they present

    return true;
}

For notification of a change in the state of UiActions, the register listens for a change in the current UI Context, as well as a notification from ModuleUiActions

...
    uicontextResolver()->currentUiContextChanged().onNotify(this, [this]() {
        updateEnabledAll();
    });
...
    module->actionEnabledChanged().onReceive(this, [this](const ActionCodeList& codes) {
        updateEnabled(codes);
        m_actionStateChanged.send(codes);
    });

    module->actionCheckedChanged().onReceive(this, [this](const ActionCodeList& codes) {
        updateChecked(codes);
        m_actionStateChanged.send(codes);
    });
...        

UI Context

To understand where the users is now and what they are doing, we introduced the concept of UI Context. The UI Context is mainly used to determine what UiActions are currently available, and it is also of primary importance for determining what action should be performed when shortcuts are activated, if several actions are assigned to one shortcut for different contexts.

The list of current contexts is in the context/uicontext.h file

namespace mu::context {
//! NOTE Determines where to be, what the user is doing

// common ui (re declared for convenience)
static constexpr ui::UiContext UiCtxUnknown = ui::UiCtxUnknown;
static constexpr ui::UiContext UiCtxAny = ui::UiCtxAny;

// screens
static constexpr ui::UiContext UiCtxNotationOpened = "UiCtxNotationOpened";
static constexpr ui::UiContext UiCtxHomeOpened = "UiCtxHomeOpened";

// common
static constexpr ui::UiContext UiCtxPlaying= "UiCtxPlaying";

// notation detail
static constexpr ui::UiContext UiCtxNotationHasSelection = "UiCtxNotationHasSelection";
static constexpr ui::UiContext UiCtxNotationTextEditing = "UiCtxNotationTextEditing";
}

This list can be expanded as needed.

The logic for determining the current UI Context is in the context::UiContextResolver class (located in the context module), which implements the ui::IUiContextResolver interface (located in the ui module). With this interface, we can get the current UI Context

ui::UiContext uicontext = uicontextResolver()->currentUiContext();

And also context resolver contains the logic for matching contexts (it is more complex than just comparison)

    if (!ctxResolver->match(currentCtx, action.context)) {
        state.enabled = false;
    }

And notifies about the change in the ui context

    uicontextResolver()->currentUiContextChanged().onNotify(this, [this]() {
        ...
    });

For each UiAction can be set a UI Context in which the action is logically available. At the moment, we can set only one value, perhaps in the future, if necessary, we can modify and set several values with the conditions or and and.

⚠️ **GitHub.com Fallback** ⚠️