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.
|
/// This is an abstract class that serves as a base class for dismiss actions.
|
||||||
abstract class DismissAction extends Action<DismissIntent> {}
|
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.
|
// Default shortcuts for the web platform.
|
||||||
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// 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.
|
// On the web, enter activates buttons, but not other controls.
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ButtonActivateIntent(),
|
||||||
|
|
||||||
@ -1100,6 +1105,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
PreviousFocusIntent: PreviousFocusAction(),
|
PreviousFocusIntent: PreviousFocusAction(),
|
||||||
DirectionalFocusIntent: DirectionalFocusAction(),
|
DirectionalFocusIntent: DirectionalFocusAction(),
|
||||||
ScrollIntent: ScrollAction(),
|
ScrollIntent: ScrollAction(),
|
||||||
|
PrioritizedIntents: PrioritizedAction(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -579,5 +579,86 @@ void main() {
|
|||||||
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
|
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
|
||||||
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
|
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