Add PrioritizedIntents to support multiple shortcut configurations (#72560)
This commit is contained in:
parent
542084d088
commit
3fd3447f96
@ -1321,3 +1321,50 @@ class DismissIntent extends Intent {
|
||||
///
|
||||
/// This is an abstract class that serves as a base class for dismiss actions.
|
||||
abstract class DismissAction extends Action<DismissIntent> {}
|
||||
|
||||
/// An [Intent] that evaluates a series of specified [orderedIntents] for
|
||||
/// execution.
|
||||
class PrioritizedIntents extends Intent {
|
||||
/// Creates a set of const [PrioritizedIntents].
|
||||
const PrioritizedIntents({
|
||||
required this.orderedIntents,
|
||||
}) : assert(orderedIntents != null);
|
||||
|
||||
/// List of intents to be evaluated in order for execution. When an
|
||||
/// [Action.isEnabled] returns true, that action will be invoked and
|
||||
/// progression through the ordered intents stops.
|
||||
final List<Intent> orderedIntents;
|
||||
}
|
||||
|
||||
/// An [Action] that iterates through a list of [Intent]s, invoking the first
|
||||
/// that is enabled.
|
||||
class PrioritizedAction extends Action<PrioritizedIntents> {
|
||||
late Action<dynamic> _selectedAction;
|
||||
late Intent _selectedIntent;
|
||||
|
||||
@override
|
||||
bool isEnabled(PrioritizedIntents intent) {
|
||||
final FocusNode? focus = primaryFocus;
|
||||
if (focus == null || focus.context == null)
|
||||
return false;
|
||||
for (final Intent candidateIntent in intent.orderedIntents) {
|
||||
final Action<Intent>? candidateAction = Actions.maybeFind<Intent>(
|
||||
focus.context!,
|
||||
intent: candidateIntent,
|
||||
);
|
||||
if (candidateAction != null && candidateAction.isEnabled(candidateIntent)) {
|
||||
_selectedAction = candidateAction;
|
||||
_selectedIntent = candidateIntent;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Object? invoke(PrioritizedIntents intent) {
|
||||
assert(_selectedAction != null);
|
||||
assert(_selectedIntent != null);
|
||||
_selectedAction.invoke(_selectedIntent);
|
||||
}
|
||||
}
|
||||
|
@ -1024,7 +1024,12 @@ class WidgetsApp extends StatefulWidget {
|
||||
// Default shortcuts for the web platform.
|
||||
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
||||
// Activation
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents(
|
||||
orderedIntents: <Intent>[
|
||||
ActivateIntent(),
|
||||
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||
]
|
||||
),
|
||||
// On the web, enter activates buttons, but not other controls.
|
||||
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
|
||||
|
||||
@ -1100,6 +1105,7 @@ class WidgetsApp extends StatefulWidget {
|
||||
PreviousFocusIntent: PreviousFocusAction(),
|
||||
DirectionalFocusIntent: DirectionalFocusAction(),
|
||||
ScrollIntent: ScrollAction(),
|
||||
PrioritizedIntents: PrioritizedAction(),
|
||||
};
|
||||
|
||||
@override
|
||||
|
@ -579,5 +579,86 @@ void main() {
|
||||
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
|
||||
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
|
||||
});
|
||||
|
||||
testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
bool? value = true;
|
||||
Widget buildApp() {
|
||||
return MaterialApp(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents(
|
||||
orderedIntents: <Intent>[
|
||||
ActivateIntent(),
|
||||
ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||
]
|
||||
),
|
||||
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||
},
|
||||
home: Material(
|
||||
child: Center(
|
||||
child: ListView(
|
||||
primary: true,
|
||||
children: <Widget> [
|
||||
StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Checkbox(
|
||||
value: value,
|
||||
onChanged: (bool? newValue) => setState(() { value = newValue; }),
|
||||
focusColor: Colors.orange[500],
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
color: Colors.blue,
|
||||
height: 1000,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
tester.binding.focusManager.primaryFocus!.toStringShort(),
|
||||
equalsIgnoringHashCodes('FocusScopeNode#00000(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])'),
|
||||
);
|
||||
final ScrollController controller = PrimaryScrollController.of(
|
||||
tester.element(find.byType(ListView))
|
||||
)!;
|
||||
expect(controller.position.pixels, 0.0);
|
||||
expect(value, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
// ScrollView scrolls
|
||||
expect(controller.position.pixels, 448.0);
|
||||
expect(value, isTrue);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.pageUp);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
// Focus is now on the checkbox.
|
||||
expect(
|
||||
tester.binding.focusManager.primaryFocus!.toStringShort(),
|
||||
equalsIgnoringHashCodes('FocusNode#00000([PRIMARY FOCUS])'),
|
||||
);
|
||||
expect(value, isTrue);
|
||||
expect(controller.position.pixels, 0.0);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
// Checkbox is toggled, scroll view does not scroll.
|
||||
expect(value, isFalse);
|
||||
expect(controller.position.pixels, 0.0);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(value, isTrue);
|
||||
expect(controller.position.pixels, 0.0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user