resolveActiveKeyBindings function

List<ActiveKeyBinding> resolveActiveKeyBindings(
  1. FocusManager manager
)

Resolves the discoverable key bindings active in manager's focus context.

Resolution follows the same rules as key dispatch: the deepest local binding wins each sequence, and the walk ends at a KeyBindings with KeyBindings.modal set, such as the one around a dialog shown with context.present, so bindings beyond it are left out. The modal scope's own bindings are listed, including one that lets its key through with KeyBindingEvent.bubble: whether a handler bubbles is decided when it runs, so the key is listed under that binding's label. Resolution also applies the framework's user-facing discovery rules:

  • bindings need an explicit KeyBinding.label;
  • disabled and KeyBinding.hideFromHintBar bindings are omitted;
  • bare printable sequences swallowed by a focused text input are omitted;
  • multi-alias bindings remain visible through any alias that can fire;
  • duplicate and shadowed aliases are removed using canonical sequence identity, not their rendered label.

The returned list is deepest-first and immutable. This is the canonical resolution API for hint bars, help overlays, and keymap inspection; those surfaces should not independently walk FocusManager.activeChain.

A command's shortcut is enabled by predicates over app state that no rebuild tracks. Called from a widget's build, the resolution remembers what they answered for that widget, and the focus manager asks them again at the start of each frame: when one answers differently, the widget rebuilds. A resolution outside a build remembers nothing.

Implementation

List<ActiveKeyBinding> resolveActiveKeyBindings(FocusManager manager) {
  final result = <ActiveKeyBinding>[];
  // Canonical sequence identity mirrors dispatch. Differently spelled aliases
  // for the same firing event must not evade deeper-binding precedence.
  final seenSequences = <KeySequence>{};
  final textFocused = manager.focusedNodeClaimsText;
  List<(KeyBinding, bool)>? liveAnswers;

  void consider(KeyBinding binding) {
    if (binding.label == null) return;
    if (binding.hideFromHintBar) return;
    final availability = binding._availability;
    final reader = Element.current;
    if (availability != null && reader != null) {
      dependOnListenable(reader, availability);
    }
    final enabled = binding.enabled;
    if (binding.isLive) (liveAnswers ??= []).add((binding, enabled));
    if (!enabled) return;

    final firable = [
      for (final sequence in binding.sequences)
        if (!textFocused || !sequence.isShadowedByTextInput) sequence,
    ];
    if (firable.isEmpty) return;

    final owned = <KeySequence>[];
    for (final sequence in firable) {
      if (seenSequences.contains(sequence)) continue;
      if (!owned.contains(sequence)) owned.add(sequence);
    }
    if (owned.isEmpty) return;

    // Claim every firable alias, including aliases already represented by a
    // sibling alias in [owned]. Shallower bindings cannot fire those.
    seenSequences.addAll(firable);
    result.add(ActiveKeyBinding._(binding, owned));
  }

  for (final node in manager.activeChain()) {
    final source = node.bindingSource;
    if (source == null) continue;
    for (final binding in source.activeBindings) {
      consider(binding);
    }
    // Dispatch stops here too, for every key no binding at this scope
    // handled (see `InputDispatcher`'s plain dispatch).
    if (source.isModalScope) break;
  }
  manager.recordLiveAnswers(liveAnswers);
  return List<ActiveKeyBinding>.unmodifiable(result);
}