Unverified Commit 0cb9f704 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Menu bar accelerators (#114852)

* Add MenuMenuAcceleratorLabel to support accelerators.

* Review Changes

* Review Changed

* Fix default label builder to use characters

* Remove golden test that shouldn't have been there.
parent db631f14
...@@ -29,6 +29,7 @@ class _HomeState extends State<Home> { ...@@ -29,6 +29,7 @@ class _HomeState extends State<Home> {
TextDirection _textDirection = TextDirection.ltr; TextDirection _textDirection = TextDirection.ltr;
double _extraPadding = 0; double _extraPadding = 0;
bool _addItem = false; bool _addItem = false;
bool _accelerators = true;
bool _transparent = false; bool _transparent = false;
bool _funkyTheme = false; bool _funkyTheme = false;
...@@ -99,6 +100,7 @@ class _HomeState extends State<Home> { ...@@ -99,6 +100,7 @@ class _HomeState extends State<Home> {
children: <Widget>[ children: <Widget>[
_TestMenus( _TestMenus(
menuController: _controller, menuController: _controller,
accelerators: _accelerators,
addItem: _addItem, addItem: _addItem,
), ),
Expanded( Expanded(
...@@ -107,6 +109,7 @@ class _HomeState extends State<Home> { ...@@ -107,6 +109,7 @@ class _HomeState extends State<Home> {
menuController: _controller, menuController: _controller,
density: _density, density: _density,
addItem: _addItem, addItem: _addItem,
accelerators: _accelerators,
transparent: _transparent, transparent: _transparent,
funkyTheme: _funkyTheme, funkyTheme: _funkyTheme,
extraPadding: _extraPadding, extraPadding: _extraPadding,
...@@ -131,6 +134,11 @@ class _HomeState extends State<Home> { ...@@ -131,6 +134,11 @@ class _HomeState extends State<Home> {
_addItem = value; _addItem = value;
}); });
}, },
onAcceleratorsChanged: (bool value) {
setState(() {
_accelerators = value;
});
},
onTransparentChanged: (bool value) { onTransparentChanged: (bool value) {
setState(() { setState(() {
_transparent = value; _transparent = value;
...@@ -159,12 +167,14 @@ class _Controls extends StatefulWidget { ...@@ -159,12 +167,14 @@ class _Controls extends StatefulWidget {
required this.textDirection, required this.textDirection,
required this.extraPadding, required this.extraPadding,
this.addItem = false, this.addItem = false,
this.accelerators = true,
this.transparent = false, this.transparent = false,
this.funkyTheme = false, this.funkyTheme = false,
required this.onDensityChanged, required this.onDensityChanged,
required this.onTextDirectionChanged, required this.onTextDirectionChanged,
required this.onExtraPaddingChanged, required this.onExtraPaddingChanged,
required this.onAddItemChanged, required this.onAddItemChanged,
required this.onAcceleratorsChanged,
required this.onTransparentChanged, required this.onTransparentChanged,
required this.onFunkyThemeChanged, required this.onFunkyThemeChanged,
required this.menuController, required this.menuController,
...@@ -174,12 +184,14 @@ class _Controls extends StatefulWidget { ...@@ -174,12 +184,14 @@ class _Controls extends StatefulWidget {
final TextDirection textDirection; final TextDirection textDirection;
final double extraPadding; final double extraPadding;
final bool addItem; final bool addItem;
final bool accelerators;
final bool transparent; final bool transparent;
final bool funkyTheme; final bool funkyTheme;
final ValueChanged<VisualDensity> onDensityChanged; final ValueChanged<VisualDensity> onDensityChanged;
final ValueChanged<TextDirection> onTextDirectionChanged; final ValueChanged<TextDirection> onTextDirectionChanged;
final ValueChanged<double> onExtraPaddingChanged; final ValueChanged<double> onExtraPaddingChanged;
final ValueChanged<bool> onAddItemChanged; final ValueChanged<bool> onAddItemChanged;
final ValueChanged<bool> onAcceleratorsChanged;
final ValueChanged<bool> onTransparentChanged; final ValueChanged<bool> onTransparentChanged;
final ValueChanged<bool> onFunkyThemeChanged; final ValueChanged<bool> onFunkyThemeChanged;
final MenuController menuController; final MenuController menuController;
...@@ -199,11 +211,9 @@ class _ControlsState extends State<_Controls> { ...@@ -199,11 +211,9 @@ class _ControlsState extends State<_Controls> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Center(
color: Colors.lightBlueAccent, child: SingleChildScrollView(
alignment: Alignment.center,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
MenuAnchor( MenuAnchor(
childFocusNode: _focusNode, childFocusNode: _focusNode,
...@@ -215,7 +225,7 @@ class _ControlsState extends State<_Controls> { ...@@ -215,7 +225,7 @@ class _ControlsState extends State<_Controls> {
onPressed: () { onPressed: () {
_itemSelected(TestMenu.standaloneMenu1); _itemSelected(TestMenu.standaloneMenu1);
}, },
child: Text(TestMenu.standaloneMenu1.label), child: MenuAcceleratorLabel(TestMenu.standaloneMenu1.label),
), ),
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.send), leadingIcon: const Icon(Icons.send),
...@@ -223,7 +233,7 @@ class _ControlsState extends State<_Controls> { ...@@ -223,7 +233,7 @@ class _ControlsState extends State<_Controls> {
onPressed: () { onPressed: () {
_itemSelected(TestMenu.standaloneMenu2); _itemSelected(TestMenu.standaloneMenu2);
}, },
child: Text(TestMenu.standaloneMenu2.label), child: MenuAcceleratorLabel(TestMenu.standaloneMenu2.label),
), ),
], ],
builder: (BuildContext context, MenuController controller, Widget? child) { builder: (BuildContext context, MenuController controller, Widget? child) {
...@@ -239,7 +249,7 @@ class _ControlsState extends State<_Controls> { ...@@ -239,7 +249,7 @@ class _ControlsState extends State<_Controls> {
child: child!, child: child!,
); );
}, },
child: const Text('Open Menu'), child: const MenuAcceleratorLabel('Open Menu'),
), ),
ConstrainedBox( ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 400),
...@@ -323,6 +333,22 @@ class _ControlsState extends State<_Controls> { ...@@ -323,6 +333,22 @@ class _ControlsState extends State<_Controls> {
const Text('Add Item') const Text('Add Item')
], ],
), ),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Checkbox(
value: widget.accelerators,
onChanged: (bool? value) {
if (value ?? false) {
widget.onAcceleratorsChanged(true);
} else {
widget.onAcceleratorsChanged(false);
}
},
),
const Text('Enable Accelerators')
],
),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
...@@ -359,6 +385,7 @@ class _ControlsState extends State<_Controls> { ...@@ -359,6 +385,7 @@ class _ControlsState extends State<_Controls> {
), ),
], ],
), ),
),
); );
} }
...@@ -412,10 +439,12 @@ class _TestMenus extends StatefulWidget { ...@@ -412,10 +439,12 @@ class _TestMenus extends StatefulWidget {
const _TestMenus({ const _TestMenus({
required this.menuController, required this.menuController,
this.addItem = false, this.addItem = false,
this.accelerators = false,
}); });
final MenuController menuController; final MenuController menuController;
final bool addItem; final bool addItem;
final bool accelerators;
@override @override
State<_TestMenus> createState() => _TestMenusState(); State<_TestMenus> createState() => _TestMenusState();
...@@ -439,8 +468,8 @@ class _TestMenusState extends State<_TestMenus> { ...@@ -439,8 +468,8 @@ class _TestMenusState extends State<_TestMenus> {
debugPrint('App: Closed item ${item.label}'); debugPrint('App: Closed item ${item.label}');
} }
void _setRadio(TestMenu item) { void _setRadio(TestMenu? item) {
debugPrint('App: Set Radio item ${item.label}'); debugPrint('App: Set Radio item ${item?.label}');
setState(() { setState(() {
radioValue = item; radioValue = item;
}); });
...@@ -519,251 +548,245 @@ class _TestMenusState extends State<_TestMenus> { ...@@ -519,251 +548,245 @@ class _TestMenusState extends State<_TestMenus> {
Expanded( Expanded(
child: MenuBar( child: MenuBar(
controller: widget.menuController, controller: widget.menuController,
children: <Widget>[ children: createTestMenus(
SubmenuButton( onPressed: _itemSelected,
onOpen: () { onOpen: _openItem,
_openItem(TestMenu.mainMenu1); onClose: _closeItem,
}, onCheckboxChanged: (TestMenu menu, bool? value) {
onClose: () { _setCheck(menu);
_closeItem(TestMenu.mainMenu1);
}, },
onRadioChanged: _setRadio,
checkboxValue: checkboxState,
radioValue: radioValue,
menuController: widget.menuController,
textEditingController: textController,
includeExtraGroups: widget.addItem,
accelerators: widget.accelerators,
),
),
),
],
);
}
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu, bool?)? onCheckboxChanged,
void Function(TestMenu?)? onRadioChanged,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool? checkboxValue,
TestMenu? radioValue,
MenuController? menuController,
TextEditingController? textEditingController,
bool includeExtraGroups = false,
bool accelerators = false,
}) {
Widget submenuButton(
TestMenu menu, {
required List<Widget> menuChildren,
}) {
return SubmenuButton(
onOpen: onOpen != null ? () => onOpen(menu) : null,
onClose: onClose != null ? () => onClose(menu) : null,
menuChildren: menuChildren,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
Widget menuItemButton(
TestMenu menu, {
bool enabled = true,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
return MenuItemButton(
key: key,
onPressed: enabled && onPressed != null ? () => onPressed(menu) : null,
shortcut: shortcuts[menu],
leadingIcon: leadingIcon,
trailingIcon: trailingIcon,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
Widget checkboxMenuButton(
TestMenu menu, {
bool enabled = true,
bool tristate = false,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
return CheckboxMenuButton(
key: key,
value: checkboxValue,
tristate: tristate,
onChanged: enabled && onCheckboxChanged != null ? (bool? value) => onCheckboxChanged(menu, value) : null,
shortcut: menu.shortcut,
trailingIcon: trailingIcon,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
Widget radioMenuButton(
TestMenu menu, {
bool enabled = true,
bool toggleable = false,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
return RadioMenuButton<TestMenu>(
key: key,
groupValue: radioValue,
value: menu,
toggleable: toggleable,
onChanged: enabled && onRadioChanged != null ? onRadioChanged : null,
shortcut: menu.shortcut,
trailingIcon: trailingIcon,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
final List<Widget> result = <Widget>[
submenuButton(
TestMenu.mainMenu1,
menuChildren: <Widget>[ menuChildren: <Widget>[
CheckboxMenuButton( checkboxMenuButton(
value: checkboxState, TestMenu.subMenu1,
tristate: true, tristate: true,
shortcut: TestMenu.subMenu1.shortcut,
trailingIcon: const Icon(Icons.assessment), trailingIcon: const Icon(Icons.assessment),
onChanged: (bool? value) {
setState(() {
checkboxState = value;
});
_itemSelected(TestMenu.subMenu1);
},
child: Text(TestMenu.subMenu1.label),
), ),
RadioMenuButton<TestMenu>( radioMenuButton(
value: TestMenu.radioMenu1, TestMenu.radioMenu1,
groupValue: radioValue,
toggleable: true, toggleable: true,
shortcut: TestMenu.radioMenu1.shortcut,
trailingIcon: const Icon(Icons.assessment), trailingIcon: const Icon(Icons.assessment),
onChanged: (TestMenu? value) {
setState(() {
radioValue = value;
});
_itemSelected(TestMenu.radioMenu1);
},
child: Text(TestMenu.radioMenu1.label),
), ),
RadioMenuButton<TestMenu>( radioMenuButton(
value: TestMenu.radioMenu2, TestMenu.radioMenu2,
groupValue: radioValue,
toggleable: true, toggleable: true,
shortcut: TestMenu.radioMenu2.shortcut,
trailingIcon: const Icon(Icons.assessment), trailingIcon: const Icon(Icons.assessment),
onChanged: (TestMenu? value) {
setState(() {
radioValue = value;
});
_itemSelected(TestMenu.radioMenu2);
},
child: Text(TestMenu.radioMenu2.label),
), ),
RadioMenuButton<TestMenu>( radioMenuButton(
value: TestMenu.radioMenu3, TestMenu.radioMenu3,
groupValue: radioValue,
toggleable: true, toggleable: true,
shortcut: TestMenu.radioMenu3.shortcut,
trailingIcon: const Icon(Icons.assessment), trailingIcon: const Icon(Icons.assessment),
onChanged: (TestMenu? value) {
setState(() {
radioValue = value;
});
_itemSelected(TestMenu.radioMenu3);
},
child: Text(TestMenu.radioMenu3.label),
), ),
MenuItemButton( menuItemButton(
TestMenu.subMenu2,
leadingIcon: const Icon(Icons.send), leadingIcon: const Icon(Icons.send),
trailingIcon: const Icon(Icons.mail), trailingIcon: const Icon(Icons.mail),
onPressed: () {
_itemSelected(TestMenu.subMenu2);
},
child: Text(TestMenu.subMenu2.label),
), ),
], ],
child: Text(TestMenu.mainMenu1.label),
), ),
SubmenuButton( submenuButton(
onOpen: () { TestMenu.mainMenu2,
_openItem(TestMenu.mainMenu2);
},
onClose: () {
_closeItem(TestMenu.mainMenu2);
},
menuChildren: <Widget>[ menuChildren: <Widget>[
TextButton( MenuAcceleratorCallbackBinding(
child: const Text('TEST'), onInvoke: onPressed != null
onPressed: () { ? () {
_itemSelected(TestMenu.testButton); onPressed.call(TestMenu.testButton);
widget.menuController.close(); menuController?.close();
}, }
: null,
child: TextButton(
onPressed: onPressed != null
? () {
onPressed.call(TestMenu.testButton);
menuController?.close();
}
: null,
child: accelerators
? MenuAcceleratorLabel(TestMenu.testButton.acceleratorLabel)
: Text(TestMenu.testButton.label),
), ),
MenuItemButton(
shortcut: TestMenu.subMenu3.shortcut,
onPressed: () {
_itemSelected(TestMenu.subMenu3);
},
child: Text(TestMenu.subMenu3.label),
), ),
menuItemButton(TestMenu.subMenu3),
], ],
child: Text(TestMenu.mainMenu2.label),
), ),
SubmenuButton( submenuButton(
onOpen: () { TestMenu.mainMenu3,
_openItem(TestMenu.mainMenu3);
},
onClose: () {
_closeItem(TestMenu.mainMenu3);
},
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subMenu8),
child: Text(TestMenu.subMenu8.label),
onPressed: () {
_itemSelected(TestMenu.subMenu8);
},
),
], ],
child: Text(TestMenu.mainMenu3.label),
), ),
SubmenuButton( submenuButton(
onOpen: () { TestMenu.mainMenu4,
_openItem(TestMenu.mainMenu4);
},
onClose: () {
_closeItem(TestMenu.mainMenu4);
},
menuChildren: <Widget>[ menuChildren: <Widget>[
Actions( MenuItemButton(
actions: <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (ActivateIntent? intent) {
debugPrint('Activated!');
return;
},
)
},
child: MenuItemButton(
onPressed: () { onPressed: () {
debugPrint('Activated text input item with ${textController.text} as a value.'); debugPrint('Activated text input item with ${textEditingController?.text} as a value.');
}, },
child: SizedBox( child: SizedBox(
width: 200, width: 200,
child: TextField( child: TextField(
controller: textController, controller: textEditingController,
onSubmitted: (String value) { onSubmitted: (String value) {
debugPrint('String $value submitted.'); debugPrint('String $value submitted.');
}, },
), ),
), ),
), ),
), submenuButton(
SubmenuButton( TestMenu.subMenu5,
onOpen: () {
_openItem(TestMenu.subMenu5);
},
onClose: () {
_closeItem(TestMenu.subMenu5);
},
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subSubMenu1),
shortcut: TestMenu.subSubMenu1.shortcut, menuItemButton(TestMenu.subSubMenu2),
onPressed: () { if (includeExtraGroups)
_itemSelected(TestMenu.subSubMenu1); submenuButton(
}, TestMenu.subSubMenu3,
child: Text(TestMenu.subSubMenu1.label),
),
MenuItemButton(
child: Text(TestMenu.subSubMenu2.label),
onPressed: () {
_itemSelected(TestMenu.subSubMenu2);
},
),
if (widget.addItem)
SubmenuButton(
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subSubSubMenu1),
shortcut: TestMenu.subSubSubMenu1.shortcut,
onPressed: () {
_itemSelected(TestMenu.subSubSubMenu1);
},
child: Text(TestMenu.subSubSubMenu1.label),
),
], ],
child: Text(TestMenu.subSubMenu3.label),
), ),
], ],
child: Text(TestMenu.subMenu5.label),
),
MenuItemButton(
// Disabled button
shortcut: TestMenu.subMenu6.shortcut,
child: Text(TestMenu.subMenu6.label),
),
MenuItemButton(
child: Text(TestMenu.subMenu7.label),
onPressed: () {
_itemSelected(TestMenu.subMenu7);
},
),
MenuItemButton(
child: Text(TestMenu.subMenu7.label),
onPressed: () {
_itemSelected(TestMenu.subMenu7);
},
),
MenuItemButton(
child: Text(TestMenu.subMenu8.label),
onPressed: () {
_itemSelected(TestMenu.subMenu8);
},
),
],
child: Text(TestMenu.mainMenu4.label),
), ),
menuItemButton(TestMenu.subMenu6, enabled: false),
menuItemButton(TestMenu.subMenu7),
menuItemButton(TestMenu.subMenu7),
menuItemButton(TestMenu.subMenu8),
], ],
), ),
), ];
], return result;
);
}
} }
enum TestMenu { enum TestMenu {
mainMenu1('Menu 1'), mainMenu1('Menu 1'),
mainMenu2('Menu 2'), mainMenu2('M&enu &2'),
mainMenu3('Menu 3'), mainMenu3('Me&nu &3'),
mainMenu4('Menu 4'), mainMenu4('Men&u &4'),
radioMenu1('Radio Menu One', SingleActivator(LogicalKeyboardKey.digit1, control: true)), radioMenu1('Radio Menu One', SingleActivator(LogicalKeyboardKey.digit1, control: true)),
radioMenu2('Radio Menu Two', SingleActivator(LogicalKeyboardKey.digit2, control: true)), radioMenu2('Radio Menu Two', SingleActivator(LogicalKeyboardKey.digit2, control: true)),
radioMenu3('Radio Menu Three', SingleActivator(LogicalKeyboardKey.digit3, control: true)), radioMenu3('Radio Menu Three', SingleActivator(LogicalKeyboardKey.digit3, control: true)),
subMenu1('Sub Menu 1', SingleActivator(LogicalKeyboardKey.keyB, control: true)), subMenu1('Sub Menu &1', SingleActivator(LogicalKeyboardKey.keyB, control: true)),
subMenu2('Sub Menu 2'), subMenu2('Sub Menu &2'),
subMenu3('Sub Menu 3', SingleActivator(LogicalKeyboardKey.enter, control: true)), subMenu3('Sub Menu &3', SingleActivator(LogicalKeyboardKey.enter, control: true)),
subMenu4('Sub Menu 4'), subMenu4('Sub Menu &4'),
subMenu5('Sub Menu 5'), subMenu5('Sub Menu &5'),
subMenu6('Sub Menu 6', SingleActivator(LogicalKeyboardKey.tab, control: true)), subMenu6('Sub Menu &6', SingleActivator(LogicalKeyboardKey.tab, control: true)),
subMenu7('Sub Menu 7'), subMenu7('Sub Menu &7'),
subMenu8('Sub Menu 8'), subMenu8('Sub Menu &8'),
subSubMenu1('Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f10, control: true)), subSubMenu1('Sub Sub Menu &1', SingleActivator(LogicalKeyboardKey.f10, control: true)),
subSubMenu2('Sub Sub Menu 2'), subSubMenu2('Sub Sub Menu &2'),
subSubMenu3('Sub Sub Menu 3'), subSubMenu3('Sub Sub Menu &3'),
subSubSubMenu1('Sub Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f11, control: true)), subSubSubMenu1('Sub Sub Sub Menu &1', SingleActivator(LogicalKeyboardKey.f11, control: true)),
testButton('TEST button'), testButton('&TEST && &&& Button &'),
standaloneMenu1('Standalone Menu 1', SingleActivator(LogicalKeyboardKey.keyC, control: true)), standaloneMenu1('Standalone Menu &1', SingleActivator(LogicalKeyboardKey.keyC, control: true)),
standaloneMenu2('Standalone Menu 2'); standaloneMenu2('Standalone Menu &2');
const TestMenu(this.label, [this.shortcut]); const TestMenu(this.acceleratorLabel, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut; final MenuSerializableShortcut? shortcut;
final String acceleratorLabel;
// Strip the accelerator markers.
String get label => MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel);
int get acceleratorIndex {
int index = -1;
MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel, setIndex: (int i) => index = i);
return index;
}
} }
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// Flutter code sample for [MenuAcceleratorLabel].
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MenuAcceleratorApp());
class MyMenuBar extends StatelessWidget {
const MyMenuBar({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: MenuBar(
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () {
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
},
child: const MenuAcceleratorLabel('&About'),
),
MenuItemButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Saved!'),
),
);
},
child: const MenuAcceleratorLabel('&Save'),
),
MenuItemButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Quit!'),
),
);
},
child: const MenuAcceleratorLabel('&Quit'),
),
],
child: const MenuAcceleratorLabel('&File'),
),
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Magnify!'),
),
);
},
child: const MenuAcceleratorLabel('&Magnify'),
),
MenuItemButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Minify!'),
),
);
},
child: const MenuAcceleratorLabel('Mi&nify'),
),
],
child: const MenuAcceleratorLabel('&View'),
),
],
),
),
],
),
Expanded(
child: FlutterLogo(
size: MediaQuery.of(context).size.shortestSide * 0.5,
),
),
],
);
}
}
class MenuAcceleratorApp extends StatelessWidget {
const MenuAcceleratorApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Shortcuts(
shortcuts: <ShortcutActivator, Intent>{
const SingleActivator(LogicalKeyboardKey.keyT, control: true): VoidCallbackIntent(() {
debugDumpApp();
}),
},
child: const Scaffold(body: MyMenuBar()),
),
);
}
}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
/// Flutter code sample for [MenuBar] /// Flutter code sample for [MenuBar].
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
......
...@@ -49,9 +49,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> { ...@@ -49,9 +49,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Shortcuts( return Shortcuts(
shortcuts: <ShortcutActivator, Intent>{ shortcuts: const <ShortcutActivator, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(), SingleActivator(LogicalKeyboardKey.arrowUp): IncrementIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(), SingleActivator(LogicalKeyboardKey.arrowDown): DecrementIntent(),
}, },
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_api_samples/material/menu_anchor/menu_accelerator_label.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open menu', (WidgetTester tester) async {
Finder findMenu(String label) {
return find
.ancestor(
of: find.text(label, findRichText: true),
matching: find.byType(FocusScope),
)
.first;
}
await tester.pumpWidget(const example.MenuAcceleratorApp());
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyF, character: 'f');
await tester.pumpAndSettle();
await tester.pump();
expect(find.text('About', findRichText: true), findsOneWidget);
expect(
tester.getRect(findMenu('About')),
equals(const Rect.fromLTRB(4.0, 48.0, 98.0, 208.0)),
);
expect(find.text('Save', findRichText: true), findsOneWidget);
expect(find.text('Quit', findRichText: true), findsOneWidget);
expect(find.text('Magnify', findRichText: true), findsNothing);
expect(find.text('Minify', findRichText: true), findsNothing);
// Open the About dialog.
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pumpAndSettle();
expect(find.text('Save', findRichText: true), findsNothing);
expect(find.text('Quit', findRichText: true), findsNothing);
expect(find.text('Magnify', findRichText: true), findsNothing);
expect(find.text('Minify', findRichText: true), findsNothing);
expect(find.text('CLOSE'), findsOneWidget);
await tester.tap(find.text('CLOSE'));
await tester.pumpAndSettle();
expect(find.text('CLOSE'), findsNothing);
});
}
...@@ -216,7 +216,7 @@ class MenuAnchor extends StatefulWidget { ...@@ -216,7 +216,7 @@ class MenuAnchor extends StatefulWidget {
/// A list of children containing the menu items that are the contents of the /// A list of children containing the menu items that are the contents of the
/// menu surrounded by this [MenuAnchor]. /// menu surrounded by this [MenuAnchor].
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
final List<Widget> menuChildren; final List<Widget> menuChildren;
/// The widget that this [MenuAnchor] surrounds. /// The widget that this [MenuAnchor] surrounds.
...@@ -263,7 +263,6 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -263,7 +263,6 @@ class _MenuAnchorState extends State<MenuAnchor> {
// view's edges. // view's edges.
final GlobalKey _anchorKey = GlobalKey(debugLabel: kReleaseMode ? null : 'MenuAnchor'); final GlobalKey _anchorKey = GlobalKey(debugLabel: kReleaseMode ? null : 'MenuAnchor');
_MenuAnchorState? _parent; _MenuAnchorState? _parent;
bool _childIsOpen = false;
final FocusScopeNode _menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : 'MenuAnchor sub menu'); final FocusScopeNode _menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : 'MenuAnchor sub menu');
MenuController? _internalMenuController; MenuController? _internalMenuController;
final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[]; final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[];
...@@ -357,6 +356,7 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -357,6 +356,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
return _MenuAnchorMarker( return _MenuAnchorMarker(
anchorKey: _anchorKey, anchorKey: _anchorKey,
anchor: this, anchor: this,
isOpen: _isOpen,
child: child, child: child,
); );
} }
...@@ -436,16 +436,14 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -436,16 +436,14 @@ class _MenuAnchorState extends State<MenuAnchor> {
return handle; return handle;
} }
void _childChangedOpenState(bool value) { void _childChangedOpenState() {
if (_childIsOpen != value) {
_parent?._childChangedOpenState(_childIsOpen || _isOpen);
if (mounted) { if (mounted) {
_parent?._childChangedOpenState();
setState(() { setState(() {
_childIsOpen = value; // Mark dirty, but only if mounted.
}); });
} }
} }
}
void _focusButton() { void _focusButton() {
if (widget.childFocusNode == null) { if (widget.childFocusNode == null) {
...@@ -483,13 +481,14 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -483,13 +481,14 @@ class _MenuAnchorState extends State<MenuAnchor> {
// close it first. // close it first.
_close(); _close();
} }
assert(_debugMenuInfo('Opening $this at ${position ?? Offset.zero} with alignment offset ${widget.alignmentOffset ?? Offset.zero}')); assert(_debugMenuInfo(
'Opening $this at ${position ?? Offset.zero} with alignment offset ${widget.alignmentOffset ?? Offset.zero}'));
_parent?._closeChildren(); // Close all siblings. _parent?._closeChildren(); // Close all siblings.
assert(_overlayEntry == null); assert(_overlayEntry == null);
final BuildContext outerContext = context; final BuildContext outerContext = context;
_parent?._childChangedOpenState();
setState(() { setState(() {
_parent?._childChangedOpenState(true);
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (BuildContext context) { builder: (BuildContext context) {
final OverlayState overlay = Overlay.of(outerContext); final OverlayState overlay = Overlay.of(outerContext);
...@@ -509,6 +508,7 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -509,6 +508,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
// it. // it.
anchorKey: _anchorKey, anchorKey: _anchorKey,
anchor: this, anchor: this,
isOpen: _isOpen,
child: _Submenu( child: _Submenu(
anchor: this, anchor: this,
menuStyle: widget.style, menuStyle: widget.style,
...@@ -542,12 +542,10 @@ class _MenuAnchorState extends State<MenuAnchor> { ...@@ -542,12 +542,10 @@ class _MenuAnchorState extends State<MenuAnchor> {
_closeChildren(inDispose: inDispose); _closeChildren(inDispose: inDispose);
_overlayEntry?.remove(); _overlayEntry?.remove();
_overlayEntry = null; _overlayEntry = null;
if (!inDispose && mounted) { if (!inDispose) {
setState(() { // Notify that _childIsOpen changed state, but only if not
// Notify that _isOpen may have changed state, but only if not currently // currently disposing.
// disposing or unmounted. _parent?._childChangedOpenState();
_parent?._childChangedOpenState(false);
});
} }
widget.onClose?.call(); widget.onClose?.call();
} }
...@@ -651,11 +649,11 @@ class MenuController { ...@@ -651,11 +649,11 @@ class MenuController {
/// When a menu item with a submenu is clicked on, it toggles the visibility of /// When a menu item with a submenu is clicked on, it toggles the visibility of
/// the submenu. When the menu item is hovered over, the submenu will open, and /// the submenu. When the menu item is hovered over, the submenu will open, and
/// hovering over other items will close the previous menu and open the newly /// hovering over other items will close the previous menu and open the newly
/// hovered one. When those open/close transitions occur, [SubmenuButton.onOpen], /// hovered one. When those open/close transitions occur,
/// and [SubmenuButton.onClose] are called on the corresponding [SubmenuButton] child /// [SubmenuButton.onOpen], and [SubmenuButton.onClose] are called on the
/// of the menu bar. /// corresponding [SubmenuButton] child of the menu bar.
/// ///
/// {@template flutter.material.menu_bar.shortcuts_note} /// {@template flutter.material.MenuBar.shortcuts_note}
/// Menus using [MenuItemButton] can have a [SingleActivator] or /// Menus using [MenuItemButton] can have a [SingleActivator] or
/// [CharacterActivator] assigned to them as their [MenuItemButton.shortcut], /// [CharacterActivator] assigned to them as their [MenuItemButton.shortcut],
/// which will display an appropriate shortcut hint. Even though the shortcut /// which will display an appropriate shortcut hint. Even though the shortcut
...@@ -670,16 +668,17 @@ class MenuController { ...@@ -670,16 +668,17 @@ class MenuController {
/// sure that selecting a menu item and triggering the shortcut do the same /// sure that selecting a menu item and triggering the shortcut do the same
/// thing, it is recommended that they call the same callback. /// thing, it is recommended that they call the same callback.
/// ///
/// {@tool dartpad} /// {@tool dartpad} This example shows a [MenuBar] that contains a single top
/// This example shows a [MenuBar] that contains a single top level menu, /// level menu, containing three items: "About", a checkbox menu item for
/// containing three items: "About", a checkbox menu item for showing a /// showing a message, and "Quit". The items are identified with an enum value,
/// message, and "Quit". The items are identified with an enum value, and the /// and the shortcuts are registered globally with the [ShortcutRegistry].
/// shortcuts are registered globally with the [ShortcutRegistry].
/// ///
/// ** See code in examples/api/lib/material/menu_anchor/menu_bar.0.dart ** /// ** See code in examples/api/lib/material/menu_anchor/menu_bar.0.dart **
/// {@end-tool} /// {@end-tool}
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@macro flutter.material.MenuAcceleratorLabel.accelerator_sample}
///
/// See also: /// See also:
/// ///
/// * [MenuAnchor], a widget that creates a region with a submenu and shows it /// * [MenuAnchor], a widget that creates a region with a submenu and shows it
...@@ -729,7 +728,7 @@ class MenuBar extends StatelessWidget { ...@@ -729,7 +728,7 @@ class MenuBar extends StatelessWidget {
/// incorrect behaviors. Whenever the menus list is modified, a new list /// incorrect behaviors. Whenever the menus list is modified, a new list
/// object must be provided. /// object must be provided.
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
final List<Widget> children; final List<Widget> children;
@override @override
...@@ -767,7 +766,7 @@ class MenuBar extends StatelessWidget { ...@@ -767,7 +766,7 @@ class MenuBar extends StatelessWidget {
/// part of a [MenuBar], but may be used independently, or as part of a menu /// part of a [MenuBar], but may be used independently, or as part of a menu
/// created with a [MenuAnchor]. /// created with a [MenuAnchor].
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
/// ///
/// See also: /// See also:
/// ///
...@@ -829,7 +828,7 @@ class MenuItemButton extends StatefulWidget { ...@@ -829,7 +828,7 @@ class MenuItemButton extends StatefulWidget {
/// The optional shortcut that selects this [MenuItemButton]. /// The optional shortcut that selects this [MenuItemButton].
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
final MenuSerializableShortcut? shortcut; final MenuSerializableShortcut? shortcut;
/// Customizes this button's appearance. /// Customizes this button's appearance.
...@@ -1029,7 +1028,7 @@ class _MenuItemButtonState extends State<MenuItemButton> { ...@@ -1029,7 +1028,7 @@ class _MenuItemButtonState extends State<MenuItemButton> {
mergedStyle = widget.style!.merge(mergedStyle); mergedStyle = widget.style!.merge(mergedStyle);
} }
return TextButton( Widget child = TextButton(
onPressed: widget.enabled ? _handleSelect : null, onPressed: widget.enabled ? _handleSelect : null,
onHover: widget.enabled ? _handleHover : null, onHover: widget.enabled ? _handleHover : null,
onFocusChange: widget.enabled ? widget.onFocusChange : null, onFocusChange: widget.enabled ? widget.onFocusChange : null,
...@@ -1045,6 +1044,15 @@ class _MenuItemButtonState extends State<MenuItemButton> { ...@@ -1045,6 +1044,15 @@ class _MenuItemButtonState extends State<MenuItemButton> {
child: widget.child!, child: widget.child!,
), ),
); );
if (_platformSupportsAccelerators() && widget.enabled) {
child = MenuAcceleratorCallbackBinding(
onInvoke: _handleSelect,
child: child,
);
}
return child;
} }
void _handleFocusChange() { void _handleFocusChange() {
...@@ -1193,7 +1201,7 @@ class CheckboxMenuButton extends StatelessWidget { ...@@ -1193,7 +1201,7 @@ class CheckboxMenuButton extends StatelessWidget {
/// The optional shortcut that selects this [MenuItemButton]. /// The optional shortcut that selects this [MenuItemButton].
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
final MenuSerializableShortcut? shortcut; final MenuSerializableShortcut? shortcut;
/// Customizes this button's appearance. /// Customizes this button's appearance.
...@@ -1390,7 +1398,7 @@ class RadioMenuButton<T> extends StatelessWidget { ...@@ -1390,7 +1398,7 @@ class RadioMenuButton<T> extends StatelessWidget {
/// The optional shortcut that selects this [MenuItemButton]. /// The optional shortcut that selects this [MenuItemButton].
/// ///
/// {@macro flutter.material.menu_bar.shortcuts_note} /// {@macro flutter.material.MenuBar.shortcuts_note}
final MenuSerializableShortcut? shortcut; final MenuSerializableShortcut? shortcut;
/// Customizes this button's appearance. /// Customizes this button's appearance.
...@@ -1467,7 +1475,6 @@ class RadioMenuButton<T> extends StatelessWidget { ...@@ -1467,7 +1475,6 @@ class RadioMenuButton<T> extends StatelessWidget {
} }
} }
/// A menu button that displays a cascading menu. /// A menu button that displays a cascading menu.
/// ///
/// It can be used as part of a [MenuBar], or as a standalone widget. /// It can be used as part of a [MenuBar], or as a standalone widget.
...@@ -1811,6 +1818,9 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1811,6 +1818,9 @@ class _SubmenuButtonState extends State<SubmenuButton> {
} }
void toggleShowMenu(BuildContext context) { void toggleShowMenu(BuildContext context) {
if (controller._anchor == null) {
return;
}
if (controller.isOpen) { if (controller.isOpen) {
controller.close(); controller.close();
} else { } else {
...@@ -1835,7 +1845,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1835,7 +1845,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
// is already open. This means that the user has to first click to // is already open. This means that the user has to first click to
// open a menu on the menu bar before hovering allows them to traverse // open a menu on the menu bar before hovering allows them to traverse
// it. // it.
if (controller._anchor!._root._orientation == Axis.horizontal && !controller._anchor!._root._childIsOpen) { if (controller._anchor!._root._orientation == Axis.horizontal && !controller._anchor!._root._isOpen) {
return; return;
} }
...@@ -1845,7 +1855,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1845,7 +1855,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
} }
} }
return TextButton( child = TextButton(
style: mergedStyle, style: mergedStyle,
focusNode: _buttonFocusNode, focusNode: _buttonFocusNode,
onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null, onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
...@@ -1858,6 +1868,15 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1858,6 +1868,15 @@ class _SubmenuButtonState extends State<SubmenuButton> {
child: child ?? const SizedBox(), child: child ?? const SizedBox(),
), ),
); );
if (_enabled && _platformSupportsAccelerators()) {
return MenuAcceleratorCallbackBinding(
onInvoke: () => toggleShowMenu(context),
hasSubmenu: true,
child: child,
);
}
return child;
}, },
menuChildren: widget.menuChildren, menuChildren: widget.menuChildren,
child: widget.child, child: widget.child,
...@@ -1882,10 +1901,7 @@ class _SubmenuButtonState extends State<SubmenuButton> { ...@@ -1882,10 +1901,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
return resolve<EdgeInsetsGeometry?>( return resolve<EdgeInsetsGeometry?>(
(MenuStyle? style) => style?.padding, (MenuStyle? style) => style?.padding,
)?.resolve( )?.resolve(Directionality.of(context)) ?? EdgeInsets.zero;
Directionality.of(context),
) ??
EdgeInsets.zero;
} }
void _handleFocusChange() { void _handleFocusChange() {
...@@ -2154,14 +2170,18 @@ class _MenuAnchorMarker extends InheritedWidget { ...@@ -2154,14 +2170,18 @@ class _MenuAnchorMarker extends InheritedWidget {
required super.child, required super.child,
required this.anchorKey, required this.anchorKey,
required this.anchor, required this.anchor,
required this.isOpen,
}); });
final GlobalKey anchorKey; final GlobalKey anchorKey;
final _MenuAnchorState anchor; final _MenuAnchorState anchor;
final bool isOpen;
@override @override
bool updateShouldNotify(_MenuAnchorMarker oldWidget) { bool updateShouldNotify(_MenuAnchorMarker oldWidget) {
return anchorKey != oldWidget.anchorKey || anchor != anchor; return anchorKey != oldWidget.anchorKey
|| anchor != oldWidget.anchor
|| isOpen != oldWidget.isOpen;
} }
} }
...@@ -2183,7 +2203,12 @@ class _MenuBarAnchorState extends _MenuAnchorState { ...@@ -2183,7 +2203,12 @@ class _MenuBarAnchorState extends _MenuAnchorState {
@override @override
bool get _isOpen { bool get _isOpen {
// If it's a bar, then it's "open" if any of its children are open. // If it's a bar, then it's "open" if any of its children are open.
return _childIsOpen; for (final _MenuAnchorState child in _anchorChildren) {
if (child._isOpen) {
return true;
}
}
return false;
} }
@override @override
...@@ -2464,6 +2489,427 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction { ...@@ -2464,6 +2489,427 @@ class _MenuDirectionalFocusAction extends DirectionalFocusAction {
} }
} }
/// An [InheritedWidget] that provides a descendant [MenuAcceleratorLabel] with
/// the function to invoke when the accelerator is pressed.
///
/// This is used when creating your own custom menu item for use with
/// [MenuAnchor] or [MenuBar]. Provided menu items such as [MenuItemButton] and
/// [SubmenuButton] already supply this wrapper internally.
class MenuAcceleratorCallbackBinding extends InheritedWidget {
/// Create a const [MenuAcceleratorCallbackBinding].
///
/// The [child] parameter is required.
const MenuAcceleratorCallbackBinding({
super.key,
this.onInvoke,
this.hasSubmenu = false,
required super.child,
});
/// The function that pressing the accelerator defined in a descendant
/// [MenuAcceleratorLabel] will invoke.
///
/// If set to null, then the accelerator won't be enabled.
final VoidCallback? onInvoke;
/// Whether or not the associated label will host its own submenu or not.
///
/// This setting determines when accelerators are active, since accelerators
/// for menu items that open submenus shouldn't be active when the submenu is
/// open.
final bool hasSubmenu;
@override
bool updateShouldNotify(MenuAcceleratorCallbackBinding oldWidget) {
return onInvoke != oldWidget.onInvoke || hasSubmenu != oldWidget.hasSubmenu;
}
/// Returns the active [MenuAcceleratorCallbackBinding] in the given context, if any,
/// and creates a dependency relationship that will rebuild the context when
/// [onInvoke] changes.
///
/// If no [MenuAcceleratorCallbackBinding] is found, returns null.
///
/// See also:
///
/// * [of], which is similar, but asserts if no [MenuAcceleratorCallbackBinding]
/// is found.
static MenuAcceleratorCallbackBinding? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MenuAcceleratorCallbackBinding>();
}
/// Returns the active [MenuAcceleratorCallbackBinding] in the given context, and
/// creates a dependency relationship that will rebuild the context when
/// [onInvoke] changes.
///
/// If no [MenuAcceleratorCallbackBinding] is found, returns will assert in debug mode
/// and throw an exception in release mode.
///
/// See also:
///
/// * [maybeOf], which is similar, but returns null if no
/// [MenuAcceleratorCallbackBinding] is found.
static MenuAcceleratorCallbackBinding of(BuildContext context) {
final MenuAcceleratorCallbackBinding? result = maybeOf(context);
assert(() {
if (result == null) {
throw FlutterError(
'MenuAcceleratorWrapper.of() was called with a context that does not '
'contain a MenuAcceleratorWrapper in the given context.\n'
'No MenuAcceleratorWrapper ancestor could be found in the context that '
'was passed to MenuAcceleratorWrapper.of(). This can happen because '
'you are using a widget that looks for a MenuAcceleratorWrapper '
'ancestor, and do not have a MenuAcceleratorWrapper widget ancestor.\n'
'The context used was:\n'
' $context',
);
}
return true;
}());
return result!;
}
}
/// The type of builder function used for building a [MenuAcceleratorLabel]'s
/// [MenuAcceleratorLabel.builder] function.
///
/// {@template flutter.material.menu_anchor.menu_accelerator_child_builder.args}
/// The arguments to the function are as follows:
///
/// * The `context` supplies the [BuildContext] to use.
/// * The `label` is the [MenuAcceleratorLabel.label] attribute for the relevant
/// [MenuAcceleratorLabel] with the accelerator markers stripped out of it.
/// * The `index` is the index of the accelerator character within the
/// `label.characters` that applies to this accelerator. If it is -1, then the
/// accelerator should not be highlighted. Otherwise, the given character
/// should be highlighted somehow in the rendered label (typically with an
/// underscore). Importantly, `index` is not an index into the [String]
/// `label`, it is an index into the [Characters] iterable returned by
/// `label.characters`, so that it is in terms of user-visible characters
/// (a.k.a. grapheme clusters), not Unicode code points.
/// {@endtemplate}
///
/// See also:
///
/// * [MenuAcceleratorLabel.defaultLabelBuilder], which is the implementation
/// used as the default value for [MenuAcceleratorLabel.builder].
typedef MenuAcceleratorChildBuilder = Widget Function(
BuildContext context,
String label,
int index,
);
/// A widget that draws the label text for a menu item (typically a
/// [MenuItemButton] or [SubmenuButton]) and renders its child with information
/// about the currently active keyboard accelerator.
///
/// On platforms other than macOS and iOS, this widget listens for the Alt key
/// to be pressed, and when it is down, will update the label by calling the
/// builder again with the position of the accelerator in the label string.
/// While the Alt key is pressed, it registers a shortcut with the
/// [ShortcutRegistry] mapped to a [VoidCallbackIntent] containing the callback
/// defined by the nearest [MenuAcceleratorCallbackBinding].
///
/// Because the accelerators are registered with the [ShortcutRegistry], any
/// other shortcuts in the widget tree between the [primaryFocus] and the
/// [ShortcutRegistry] that define Alt-based shortcuts using the same keys will
/// take precedence over the accelerators.
///
/// Because accelerators aren't used on macOS and iOS, the label ignores the Alt
/// key on those platforms, and the [builder] is always given -1 as an
/// accelerator index. Accelerator labels are still stripped of their
/// accelerator markers.
///
/// The built-in menu items [MenuItemButton] and [SubmenuButton] already provide
/// the appropriate [MenuAcceleratorCallbackBinding], so unless you are creating
/// your own custom menu item type that takes a [MenuAcceleratorLabel], it is
/// not necessary to provide one.
///
/// {@template flutter.material.MenuAcceleratorLabel.accelerator_sample}
/// {@tool dartpad} This example shows a [MenuBar] that handles keyboard
/// accelerators using [MenuAcceleratorLabel]. To use the accelerators, press
/// the Alt key to see which letters are underlined in the menu bar, and then
/// press the appropriate letter. Accelerators are not supported on macOS or iOS
/// since those platforms don't support them natively, so this demo will only
/// show a regular Material menu bar on those platforms.
///
/// ** See code in examples/api/lib/material/menu_anchor/menu_accelerator_label.0.dart **
/// {@end-tool}
/// {@endtemplate}
class MenuAcceleratorLabel extends StatefulWidget {
/// Creates a const [MenuAcceleratorLabel].
///
/// The [label] parameter is required.
const MenuAcceleratorLabel(
this.label, {
super.key,
this.builder = defaultLabelBuilder,
});
/// The label string that should be displayed.
///
/// The label string provides the label text, as well as the possible
/// characters which could be used as accelerators in the menu system.
///
/// {@template flutter.material.menu_anchor.menu_accelerator_label.label}
/// To indicate which letters in the label are to be used as accelerators, add
/// an "&" character before the character in the string. If more than one
/// character has an "&" in front of it, then the characters appearing earlier
/// in the string are preferred. To represent a literal "&", insert "&&" into
/// the string. All other ampersands will be removed from the string before
/// calling [MenuAcceleratorLabel.builder]. Bare ampersands at the end of the
/// string or before whitespace are stripped and ignored.
/// {@endtemplate}
///
/// See also:
///
/// * [displayLabel], which returns the [label] with all of the ampersands
/// stripped out of it, and double ampersands converted to ampersands.
/// * [stripAcceleratorMarkers], which returns the supplied string with all of
/// the ampersands stripped out of it, and double ampersands converted to
/// ampersands, and optionally calls a callback with the index of the
/// accelerator character found.
final String label;
/// Returns the [label] with any accelerator markers removed.
///
/// This getter just calls [stripAcceleratorMarkers] with the [label].
String get displayLabel => stripAcceleratorMarkers(label);
/// The optional [MenuAcceleratorChildBuilder] which is used to build the
/// widget that displays the label itself.
///
/// The [defaultLabelBuilder] function serves as the default value for
/// [builder], rendering the label as a [RichText] widget with appropriate
/// [TextSpan]s for rendering the label with an underscore under the selected
/// accelerator for the label when accelerators have been activated.
///
/// {@macro flutter.material.menu_anchor.menu_accelerator_child_builder.args}
///
/// When writing the builder function, it's not necessary to take the current
/// platform into account. On platforms which don't support accelerators (e.g.
/// macOS and iOS), the passed accelerator index will always be -1, and the
/// accelerator markers will already be stripped.
final MenuAcceleratorChildBuilder builder;
/// Whether [label] contains an accelerator definition.
///
/// {@macro flutter.material.menu_anchor.menu_accelerator_label.label}
bool get hasAccelerator => RegExp(r'&(?!([&\s]|$))').hasMatch(label);
/// Serves as the default value for [builder], rendering the label as a
/// [RichText] widget with appropriate [TextSpan]s for rendering the label
/// with an underscore under the selected accelerator for the label when the
/// [index] is non-negative, and a [Text] widget when the [index] is negative.
///
/// {@macro flutter.material.menu_anchor.menu_accelerator_child_builder.args}
static Widget defaultLabelBuilder(
BuildContext context,
String label,
int index,
) {
if (index < 0) {
return Text(label);
}
final TextStyle defaultStyle = DefaultTextStyle.of(context).style;
final Characters characters = label.characters;
return RichText(
text: TextSpan(
children: <TextSpan>[
if (index > 0)
TextSpan(text: characters.getRange(0, index).toString(), style: defaultStyle),
TextSpan(
text: characters.getRange(index, index + 1).toString(),
style: defaultStyle.copyWith(decoration: TextDecoration.underline),
),
if (index < characters.length - 1)
TextSpan(text: characters.getRange(index + 1).toString(), style: defaultStyle),
],
),
);
}
/// Strips out any accelerator markers from the given [label], and unescapes
/// any escaped ampersands.
///
/// If [setIndex] is supplied, it will be called before this function returns
/// with the index in the returned string of the accelerator character.
///
/// {@macro flutter.material.menu_anchor.menu_accelerator_label.label}
static String stripAcceleratorMarkers(String label, {void Function(int index)? setIndex}) {
int quotedAmpersands = 0;
final StringBuffer displayLabel = StringBuffer();
int acceleratorIndex = -1;
// Use characters so that we don't split up surrogate pairs and interpret
// them incorrectly.
final Characters labelChars = label.characters;
final Characters ampersand = '&'.characters;
bool lastWasAmpersand = false;
for (int i = 0; i < labelChars.length; i += 1) {
// Stop looking one before the end, since a single ampersand at the end is
// just treated as a quoted ampersand.
final Characters character = labelChars.characterAt(i);
if (lastWasAmpersand) {
lastWasAmpersand = false;
displayLabel.write(character);
continue;
}
if (character != ampersand) {
displayLabel.write(character);
continue;
}
if (i == labelChars.length - 1) {
// Strip bare ampersands at the end of a string.
break;
}
lastWasAmpersand = true;
final Characters acceleratorCharacter = labelChars.characterAt(i + 1);
if (acceleratorIndex == -1 && acceleratorCharacter != ampersand &&
acceleratorCharacter.toString().trim().isNotEmpty) {
// Don't set the accelerator index if the character is an ampersand,
// or whitespace.
acceleratorIndex = i - quotedAmpersands;
}
// As we encounter '&<character>' pairs, the following indices must be
// adjusted so that they correspond with indices in the stripped string.
quotedAmpersands += 1;
}
setIndex?.call(acceleratorIndex);
return displayLabel.toString();
}
@override
State<MenuAcceleratorLabel> createState() => _MenuAcceleratorLabelState();
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return '$MenuAcceleratorLabel("$label")';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('label', label));
}
}
class _MenuAcceleratorLabelState extends State<MenuAcceleratorLabel> {
late String _displayLabel;
int _acceleratorIndex = -1;
MenuAcceleratorCallbackBinding? _binding;
_MenuAnchorState? _anchor;
ShortcutRegistry? _shortcutRegistry;
ShortcutRegistryEntry? _shortcutRegistryEntry;
bool _showAccelerators = false;
@override
void initState() {
super.initState();
if (_platformSupportsAccelerators()) {
_showAccelerators = _altIsPressed();
HardwareKeyboard.instance.addHandler(_handleKeyEvent);
}
_updateDisplayLabel();
}
@override
void dispose() {
assert(_platformSupportsAccelerators() || _shortcutRegistryEntry == null);
_displayLabel = '';
if (_platformSupportsAccelerators()) {
_shortcutRegistryEntry?.dispose();
_shortcutRegistryEntry = null;
_shortcutRegistry = null;
_anchor = null;
HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
}
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_platformSupportsAccelerators()) {
return;
}
_binding = MenuAcceleratorCallbackBinding.maybeOf(context);
_anchor = _MenuAnchorState._maybeOf(context);
_shortcutRegistry = ShortcutRegistry.maybeOf(context);
_updateAcceleratorShortcut();
}
@override
void didUpdateWidget(MenuAcceleratorLabel oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.label != oldWidget.label) {
_updateDisplayLabel();
}
}
static bool _altIsPressed() {
return HardwareKeyboard.instance.logicalKeysPressed.intersection(
<LogicalKeyboardKey>{
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.alt,
},
).isNotEmpty;
}
bool _handleKeyEvent(KeyEvent event) {
assert(_platformSupportsAccelerators());
final bool altIsPressed = _altIsPressed();
if (altIsPressed != _showAccelerators) {
setState(() {
_showAccelerators = altIsPressed;
_updateAcceleratorShortcut();
});
}
// Just listening, does't ever handle a key.
return false;
}
void _updateAcceleratorShortcut() {
assert(_platformSupportsAccelerators());
_shortcutRegistryEntry?.dispose();
_shortcutRegistryEntry = null;
// Before registering an accelerator as a shortcut it should meet these
// conditions:
//
// 1) Is showing accelerators (i.e. Alt key is down).
// 2) Has an accelerator marker in the label.
// 3) Has an associated action callback for the label (from the
// MenuAcceleratorCallbackBinding).
// 4) Is part of an anchor that either doesn't have a submenu, or doesn't
// have any submenus currently open (only the "deepest" open menu should
// have accelerator shortcuts registered).
assert(_displayLabel != null);
if (_showAccelerators && _acceleratorIndex != -1 && _binding?.onInvoke != null && !(_binding!.hasSubmenu && (_anchor?._isOpen ?? false))) {
final String acceleratorCharacter = _displayLabel[_acceleratorIndex].toLowerCase();
_shortcutRegistryEntry = _shortcutRegistry?.addAll(
<ShortcutActivator, Intent>{
CharacterActivator(acceleratorCharacter, alt: true): VoidCallbackIntent(_binding!.onInvoke!),
},
);
}
}
void _updateDisplayLabel() {
_displayLabel = MenuAcceleratorLabel.stripAcceleratorMarkers(
widget.label,
setIndex: (int index) {
_acceleratorIndex = index;
},
);
}
@override
Widget build(BuildContext context) {
final int index = _showAccelerators ? _acceleratorIndex : -1;
return widget.builder(context, _displayLabel, index);
}
}
/// A label widget that is used as the label for a [MenuItemButton] or /// A label widget that is used as the label for a [MenuItemButton] or
/// [SubmenuButton]. /// [SubmenuButton].
/// ///
...@@ -3061,6 +3507,23 @@ bool _debugMenuInfo(String message, [Iterable<String>? details]) { ...@@ -3061,6 +3507,23 @@ bool _debugMenuInfo(String message, [Iterable<String>? details]) {
return true; return true;
} }
bool _platformSupportsAccelerators() {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return true;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
// On iOS and macOS, pressing the Option key (a.k.a. the Alt key) causes a
// different set of characters to be generated, and the native menus don't
// support accelerators anyhow, so we just disable accelerators on these
// platforms.
return false;
}
}
// BEGIN GENERATED TOKEN PROPERTIES - Menu // BEGIN GENERATED TOKEN PROPERTIES - Menu
// Do not edit by hand. The code between the "BEGIN GENERATED" and // Do not edit by hand. The code between the "BEGIN GENERATED" and
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:collection'; import 'dart:collection';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'actions.dart'; import 'actions.dart';
...@@ -577,24 +578,43 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S ...@@ -577,24 +578,43 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// ** See code in examples/api/lib/widgets/shortcuts/character_activator.0.dart ** /// ** See code in examples/api/lib/widgets/shortcuts/character_activator.0.dart **
/// {@end-tool} /// {@end-tool}
/// ///
/// The [alt], [control], and [meta] flags represent whether the respective
/// modifier keys should be held (true) or released (false). They default to
/// false. [CharacterActivator] cannot check shifted keys, since the Shift key
/// affects the resulting character, and will accept whether either of the
/// Shift keys are pressed or not, as long as the key event produces the
/// correct character.
///
/// By default, the activator is checked on all [RawKeyDownEvent] events for
/// the [character] in combination with the requested modifier keys. If
/// `includeRepeats` is false, only the [character] events with a false
/// [RawKeyDownEvent.repeat] attribute will be considered.
///
/// {@template flutter.widgets.shortcuts.CharacterActivator.alt}
/// On macOS and iOS, the [alt] flag indicates that the Option key (⌥) is
/// pressed. Because the Option key affects the character generated on these
/// platforms, it can be unintuitive to define [CharacterActivator]s for them.
///
/// For instance, if you want the shortcut to trigger when Option+s (⌥-s) is
/// pressed, and what you intend is to trigger whenever the character 'ß' is
/// produced, you would use `CharacterActivator('ß')` or
/// `CharacterActivator('ß', alt: true)` instead of `CharacterActivator('s',
/// alt: true)`. This is because `CharacterActivator('s', alt: true)` will
/// never trigger, since the 's' character can't be produced when the Option
/// key is held down.
///
/// If what is intended is that the shortcut is triggered when Option+s (⌥-s)
/// is pressed, regardless of which character is produced, it is better to use
/// [SingleActivator], as in `SingleActivator(LogicalKeyboardKey.keyS, alt:
/// true)`.
/// {@endtemplate}
///
/// See also: /// See also:
/// ///
/// * [SingleActivator], an activator that represents a single key combined /// * [SingleActivator], an activator that represents a single key combined
/// with modifiers, such as `Ctrl+C` or `Ctrl-Right Arrow`. /// with modifiers, such as `Ctrl+C` or `Ctrl-Right Arrow`.
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator { class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
/// Triggered when the key event yields the given character. /// Triggered when the key event yields the given character.
///
/// The [alt], [control], and [meta] flags represent whether the respective
/// modifier keys should be held (true) or released (false). They default to
/// false. [CharacterActivator] cannot check Shift keys, since the shift key
/// affects the resulting character, and will accept whether either of the
/// Shift keys are pressed or not, as long as the key event produces the
/// correct character.
///
/// By default, the activator is checked on all [RawKeyDownEvent] events for
/// the [character] in combination with the requested modifier keys. If
/// `includeRepeats` is false, only the [character] events with a false
/// [RawKeyDownEvent.repeat] attribute will be considered.
const CharacterActivator(this.character, { const CharacterActivator(this.character, {
this.alt = false, this.alt = false,
this.control = false, this.control = false,
...@@ -602,36 +622,38 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement ...@@ -602,36 +622,38 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
this.includeRepeats = true, this.includeRepeats = true,
}); });
/// Whether either (or both) alt keys should be held for the [character] to /// Whether either (or both) Alt keys should be held for the [character] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// It defaults to false, meaning all Alt keys must be released when the event /// It defaults to false, meaning all Alt keys must be released when the event
/// is received in order to activate the shortcut. If it's true, then either /// is received in order to activate the shortcut. If it's true, then either
/// or both Alt keys must be pressed. /// one or both Alt keys must be pressed.
///
/// {@macro flutter.widgets.shortcuts.CharacterActivator.alt}
/// ///
/// See also: /// See also:
/// ///
/// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight]. /// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight].
final bool alt; final bool alt;
/// Whether either (or both) control keys should be held for the [character] /// Whether either (or both) Control keys should be held for the [character]
/// to activate the shortcut. /// to activate the shortcut.
/// ///
/// It defaults to false, meaning all Control keys must be released when the /// It defaults to false, meaning all Control keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then /// event is received in order to activate the shortcut. If it's true, then
/// either or both Control keys must be pressed. /// either one or both Control keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
/// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight]. /// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight].
final bool control; final bool control;
/// Whether either (or both) meta keys should be held for the [character] to /// Whether either (or both) Meta keys should be held for the [character] to
/// activate the shortcut. /// activate the shortcut.
/// ///
/// It defaults to false, meaning all Meta keys must be released when the /// It defaults to false, meaning all Meta keys must be released when the
/// event is received in order to activate the shortcut. If it's true, then /// event is received in order to activate the shortcut. If it's true, then
/// either or both Meta keys must be pressed. /// either one or both Meta keys must be pressed.
/// ///
/// See also: /// See also:
/// ///
...@@ -1150,7 +1172,7 @@ class ShortcutRegistryEntry { ...@@ -1150,7 +1172,7 @@ class ShortcutRegistryEntry {
/// [ShortcutRegistryEntry] from the [registry]. /// [ShortcutRegistryEntry] from the [registry].
@mustCallSuper @mustCallSuper
void dispose() { void dispose() {
registry._disposeToken(this); registry._disposeEntry(this);
} }
} }
...@@ -1160,11 +1182,22 @@ class ShortcutRegistryEntry { ...@@ -1160,11 +1182,22 @@ class ShortcutRegistryEntry {
/// You can reach the nearest [ShortcutRegistry] using [of] and [maybeOf]. /// You can reach the nearest [ShortcutRegistry] using [of] and [maybeOf].
/// ///
/// The registry may be listened to (with [addListener]/[removeListener]) for /// The registry may be listened to (with [addListener]/[removeListener]) for
/// change notifications when the registered shortcuts change. /// change notifications when the registered shortcuts change. Change
/// notifications take place after the the current frame is drawn, so that
/// widgets that are not descendants of the registry can listen to it (e.g. in
/// overlays).
class ShortcutRegistry with ChangeNotifier { class ShortcutRegistry with ChangeNotifier {
bool _notificationScheduled = false;
bool _disposed = false;
@override
void dispose() {
super.dispose();
_disposed = true;
}
/// Gets the combined shortcut bindings from all contexts that are registered /// Gets the combined shortcut bindings from all contexts that are registered
/// with this [ShortcutRegistry], in addition to the bindings passed to /// with this [ShortcutRegistry].
/// [ShortcutRegistry].
/// ///
/// Listeners will be notified when the value returned by this getter changes. /// Listeners will be notified when the value returned by this getter changes.
/// ///
...@@ -1172,11 +1205,12 @@ class ShortcutRegistry with ChangeNotifier { ...@@ -1172,11 +1205,12 @@ class ShortcutRegistry with ChangeNotifier {
Map<ShortcutActivator, Intent> get shortcuts { Map<ShortcutActivator, Intent> get shortcuts {
assert(ChangeNotifier.debugAssertNotDisposed(this)); assert(ChangeNotifier.debugAssertNotDisposed(this));
return <ShortcutActivator, Intent>{ return <ShortcutActivator, Intent>{
for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> entry in _tokenShortcuts.entries) for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> entry in _registeredShortcuts.entries)
...entry.value, ...entry.value,
}; };
} }
final Map<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> _tokenShortcuts =
final Map<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> _registeredShortcuts =
<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>>{}; <ShortcutRegistryEntry, Map<ShortcutActivator, Intent>>{};
/// Adds all the given shortcut bindings to this [ShortcutRegistry], and /// Adds all the given shortcut bindings to this [ShortcutRegistry], and
...@@ -1202,13 +1236,31 @@ class ShortcutRegistry with ChangeNotifier { ...@@ -1202,13 +1236,31 @@ class ShortcutRegistry with ChangeNotifier {
/// shortcuts associated with a particular entry. /// shortcuts associated with a particular entry.
ShortcutRegistryEntry addAll(Map<ShortcutActivator, Intent> value) { ShortcutRegistryEntry addAll(Map<ShortcutActivator, Intent> value) {
assert(ChangeNotifier.debugAssertNotDisposed(this)); assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(value.isNotEmpty, 'Cannot register an empty map of shortcuts');
final ShortcutRegistryEntry entry = ShortcutRegistryEntry._(this); final ShortcutRegistryEntry entry = ShortcutRegistryEntry._(this);
_tokenShortcuts[entry] = value; _registeredShortcuts[entry] = value;
assert(_debugCheckForDuplicates()); assert(_debugCheckForDuplicates());
notifyListeners(); _notifyListenersNextFrame();
return entry; return entry;
} }
// Subscriber notification has to happen in the next frame because shortcuts
// are often registered that affect things in the overlay or different parts
// of the tree, and so can cause build ordering issues if notifications happen
// during the build. The _notificationScheduled check makes sure we only
// notify once per frame.
void _notifyListenersNextFrame() {
if (!_notificationScheduled) {
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_notificationScheduled = false;
if (!_disposed) {
notifyListeners();
}
});
_notificationScheduled = true;
}
}
/// Returns the [ShortcutRegistry] that belongs to the [ShortcutRegistrar] /// Returns the [ShortcutRegistry] that belongs to the [ShortcutRegistrar]
/// which most tightly encloses the given [BuildContext]. /// which most tightly encloses the given [BuildContext].
/// ///
...@@ -1270,23 +1322,24 @@ class ShortcutRegistry with ChangeNotifier { ...@@ -1270,23 +1322,24 @@ class ShortcutRegistry with ChangeNotifier {
// registry. // registry.
void _replaceAll(ShortcutRegistryEntry entry, Map<ShortcutActivator, Intent> value) { void _replaceAll(ShortcutRegistryEntry entry, Map<ShortcutActivator, Intent> value) {
assert(ChangeNotifier.debugAssertNotDisposed(this)); assert(ChangeNotifier.debugAssertNotDisposed(this));
assert(_debugCheckTokenIsValid(entry)); assert(_debugCheckEntryIsValid(entry));
_tokenShortcuts[entry] = value; _registeredShortcuts[entry] = value;
assert(_debugCheckForDuplicates()); assert(_debugCheckForDuplicates());
notifyListeners(); _notifyListenersNextFrame();
} }
// Removes all the shortcuts associated with the given entry from this // Removes all the shortcuts associated with the given entry from this
// registry. // registry.
void _disposeToken(ShortcutRegistryEntry entry) { void _disposeEntry(ShortcutRegistryEntry entry) {
assert(_debugCheckTokenIsValid(entry)); assert(_debugCheckEntryIsValid(entry));
if (_tokenShortcuts.remove(entry) != null) { final Map<ShortcutActivator, Intent>? removedShortcut = _registeredShortcuts.remove(entry);
notifyListeners(); if (removedShortcut != null) {
_notifyListenersNextFrame();
} }
} }
bool _debugCheckTokenIsValid(ShortcutRegistryEntry entry) { bool _debugCheckEntryIsValid(ShortcutRegistryEntry entry) {
if (!_tokenShortcuts.containsKey(entry)) { if (!_registeredShortcuts.containsKey(entry)) {
if (entry.registry == this) { if (entry.registry == this) {
throw FlutterError('entry ${describeIdentity(entry)} is invalid.\n' throw FlutterError('entry ${describeIdentity(entry)} is invalid.\n'
'The entry has already been disposed of. Tokens are not valid after ' 'The entry has already been disposed of. Tokens are not valid after '
...@@ -1303,7 +1356,7 @@ class ShortcutRegistry with ChangeNotifier { ...@@ -1303,7 +1356,7 @@ class ShortcutRegistry with ChangeNotifier {
bool _debugCheckForDuplicates() { bool _debugCheckForDuplicates() {
final Map<ShortcutActivator, ShortcutRegistryEntry?> previous = <ShortcutActivator, ShortcutRegistryEntry?>{}; final Map<ShortcutActivator, ShortcutRegistryEntry?> previous = <ShortcutActivator, ShortcutRegistryEntry?>{};
for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> tokenEntry in _tokenShortcuts.entries) { for (final MapEntry<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> tokenEntry in _registeredShortcuts.entries) {
for (final ShortcutActivator shortcut in tokenEntry.value.keys) { for (final ShortcutActivator shortcut in tokenEntry.value.keys) {
if (previous.containsKey(shortcut)) { if (previous.containsKey(shortcut)) {
throw FlutterError( throw FlutterError(
...@@ -1378,10 +1431,10 @@ class _ShortcutRegistrarState extends State<ShortcutRegistrar> { ...@@ -1378,10 +1431,10 @@ class _ShortcutRegistrarState extends State<ShortcutRegistrar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Shortcuts.manager( return _ShortcutRegistrarMarker(
manager: manager,
child: _ShortcutRegistrarMarker(
registry: registry, registry: registry,
child: Shortcuts.manager(
manager: manager,
child: widget.child, child: widget.child,
), ),
); );
......
...@@ -191,11 +191,11 @@ void main() { ...@@ -191,11 +191,11 @@ void main() {
' Localizations\n' ' Localizations\n'
' MediaQuery\n' ' MediaQuery\n'
' _MediaQueryFromWindow\n' ' _MediaQueryFromWindow\n'
' _ShortcutRegistrarMarker\n'
' Semantics\n' ' Semantics\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' _ShortcutRegistrarMarker\n'
' ShortcutRegistrar\n' ' ShortcutRegistrar\n'
' TapRegionSurface\n' ' TapRegionSurface\n'
' _FocusMarker\n' ' _FocusMarker\n'
......
...@@ -31,7 +31,7 @@ void main() { ...@@ -31,7 +31,7 @@ void main() {
} }
void handleFocusChange() { void handleFocusChange() {
focusedMenu = primaryFocus?.debugLabel ?? primaryFocus?.toString(); focusedMenu = (primaryFocus?.debugLabel ?? primaryFocus).toString();
} }
setUpAll(() { setUpAll(() {
...@@ -392,6 +392,28 @@ void main() { ...@@ -392,6 +392,28 @@ void main() {
), ),
equals(const Rect.fromLTRB(112.0, 48.0, 326.0, 208.0)), equals(const Rect.fromLTRB(112.0, 48.0, 326.0, 208.0)),
); );
// Test menu bar size when not expanded.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBar(
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
expect(
tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
);
}); });
testWidgets('geometry with RTL direction', (WidgetTester tester) async { testWidgets('geometry with RTL direction', (WidgetTester tester) async {
...@@ -448,6 +470,8 @@ void main() { ...@@ -448,6 +470,8 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
MenuBar( MenuBar(
...@@ -458,12 +482,13 @@ void main() { ...@@ -458,12 +482,13 @@ void main() {
), ),
), ),
), ),
),
); );
await tester.pump(); await tester.pump();
expect( expect(
tester.getRect(find.byType(MenuBar)), tester.getRect(find.byType(MenuBar)),
equals(const Rect.fromLTRB(180.0, 0.0, 620.0, 48.0)), equals(const Rect.fromLTRB(145.0, 0.0, 655.0, 48.0)),
); );
}); });
...@@ -996,10 +1021,6 @@ void main() { ...@@ -996,10 +1021,6 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))')); expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
}); });
testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async { testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async {
...@@ -1086,10 +1107,6 @@ void main() { ...@@ -1086,10 +1107,6 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))')); expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
}); });
testWidgets('hover traversal works', (WidgetTester tester) async { testWidgets('hover traversal works', (WidgetTester tester) async {
...@@ -1235,6 +1252,243 @@ void main() { ...@@ -1235,6 +1252,243 @@ void main() {
}); });
}); });
group('Accelerators', () {
const Set<TargetPlatform> apple = <TargetPlatform>{TargetPlatform.macOS, TargetPlatform.iOS};
final Set<TargetPlatform> nonApple = TargetPlatform.values.toSet().difference(apple);
test('Accelerator markers are stripped properly', () {
const Map<String, String> expected = <String, String>{
'Plain String': 'Plain String',
'&Simple Accelerator': 'Simple Accelerator',
'&Multiple &Accelerators': 'Multiple Accelerators',
'Whitespace & Accelerators': 'Whitespace Accelerators',
'&Quoted && Ampersand': 'Quoted & Ampersand',
'Ampersand at End &': 'Ampersand at End ',
'&&Multiple Ampersands &&& &&&A &&&&B &&&&': '&Multiple Ampersands & &A &&B &&',
'Bohrium 𨨏 Code point U+28A0F': 'Bohrium 𨨏 Code point U+28A0F',
};
const List<int> expectedIndices = <int>[-1, 0, 0, -1, 0, -1, 24, -1];
const List<bool> expectedHasAccelerator = <bool>[false, true, true, false, true, false, true, false];
int acceleratorIndex = -1;
int count = 0;
for (final String key in expected.keys) {
expect(MenuAcceleratorLabel.stripAcceleratorMarkers(key, setIndex: (int index) {
acceleratorIndex = index;
}), equals(expected[key]),
reason: "'$key' label doesn't match ${expected[key]}");
expect(acceleratorIndex, equals(expectedIndices[count]),
reason: "'$key' index doesn't match ${expectedIndices[count]}");
expect(MenuAcceleratorLabel(key).hasAccelerator, equals(expectedHasAccelerator[count]),
reason: "'$key' hasAccelerator isn't ${expectedHasAccelerator[count]}");
count += 1;
}
});
testWidgets('can invoke menu items', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.pump();
// Makes sure that identical accelerators in parent menu items don't
// shadow the ones in the children.
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(closed, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(selected, equals(<TestMenu>[TestMenu.subMenu00]));
// Selecting a non-submenu item should close all the menus.
expect(find.text(TestMenu.subMenu00.label), findsNothing);
opened.clear();
closed.clear();
selected.clear();
// Invoking several levels deep.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu111]));
opened.clear();
closed.clear();
selected.clear();
}, variant: TargetPlatformVariant(nonApple));
testWidgets('can combine with regular keyboard navigation', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
// Combining accelerators and regular keyboard navigation works.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110]));
}, variant: TargetPlatformVariant(nonApple));
testWidgets('can combine with mouse', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
// Combining accelerators and regular keyboard navigation works.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'e');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '1');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.tap(find.text(TestMenu.subSubMenu112.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(closed, equals(<TestMenu>[TestMenu.subMenu11, TestMenu.mainMenu1]));
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu112]));
}, variant: TargetPlatformVariant(nonApple));
testWidgets("disabled items don't respond to accelerators", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: '5');
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
// Selecting a non-submenu item should close all the menus.
expect(find.text(TestMenu.subMenu00.label), findsNothing);
}, variant: TargetPlatformVariant(nonApple));
testWidgets("Apple platforms don't react to accelerators", (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
accelerators: true,
),
),
),
),
);
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'm');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
// Or with the option key equivalents.
await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'µ');
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyM, character: 'µ');
await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isEmpty);
expect(selected, isEmpty);
}, variant: const TargetPlatformVariant(apple));
});
group('MenuController', () { group('MenuController', () {
testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async { testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1605,7 +1859,7 @@ void main() { ...@@ -1605,7 +1859,7 @@ void main() {
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0))); expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0))); expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0))); expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 436.0, 48.0))); expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 506.0, 48.0)));
expect(menuRects[4], equals(const Rect.fromLTRB(112.0, 104.0, 326.0, 152.0))); expect(menuRects[4], equals(const Rect.fromLTRB(112.0, 104.0, 326.0, 152.0)));
}); });
...@@ -1647,7 +1901,7 @@ void main() { ...@@ -1647,7 +1901,7 @@ void main() {
expect(menuRects[0], equals(const Rect.fromLTRB(688.0, 0.0, 796.0, 48.0))); expect(menuRects[0], equals(const Rect.fromLTRB(688.0, 0.0, 796.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(580.0, 0.0, 688.0, 48.0))); expect(menuRects[1], equals(const Rect.fromLTRB(580.0, 0.0, 688.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(472.0, 0.0, 580.0, 48.0))); expect(menuRects[2], equals(const Rect.fromLTRB(472.0, 0.0, 580.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(364.0, 0.0, 472.0, 48.0))); expect(menuRects[3], equals(const Rect.fromLTRB(294.0, 0.0, 472.0, 48.0)));
expect(menuRects[4], equals(const Rect.fromLTRB(474.0, 104.0, 688.0, 152.0))); expect(menuRects[4], equals(const Rect.fromLTRB(474.0, 104.0, 688.0, 152.0)));
}); });
...@@ -1687,7 +1941,7 @@ void main() { ...@@ -1687,7 +1941,7 @@ void main() {
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0))); expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 112.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0))); expect(menuRects[1], equals(const Rect.fromLTRB(112.0, 0.0, 220.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0))); expect(menuRects[2], equals(const Rect.fromLTRB(220.0, 0.0, 328.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 436.0, 48.0))); expect(menuRects[3], equals(const Rect.fromLTRB(328.0, 0.0, 506.0, 48.0)));
expect(menuRects[4], equals(const Rect.fromLTRB(86.0, 104.0, 300.0, 152.0))); expect(menuRects[4], equals(const Rect.fromLTRB(86.0, 104.0, 300.0, 152.0)));
}); });
...@@ -1727,7 +1981,7 @@ void main() { ...@@ -1727,7 +1981,7 @@ void main() {
expect(menuRects[0], equals(const Rect.fromLTRB(188.0, 0.0, 296.0, 48.0))); expect(menuRects[0], equals(const Rect.fromLTRB(188.0, 0.0, 296.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(80.0, 0.0, 188.0, 48.0))); expect(menuRects[1], equals(const Rect.fromLTRB(80.0, 0.0, 188.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0))); expect(menuRects[2], equals(const Rect.fromLTRB(-28.0, 0.0, 80.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(-136.0, 0.0, -28.0, 48.0))); expect(menuRects[3], equals(const Rect.fromLTRB(-206.0, 0.0, -28.0, 48.0)));
expect(menuRects[4], equals(const Rect.fromLTRB(0.0, 104.0, 214.0, 152.0))); expect(menuRects[4], equals(const Rect.fromLTRB(0.0, 104.0, 214.0, 152.0)));
}); });
}); });
...@@ -1929,164 +2183,124 @@ List<Widget> createTestMenus({ ...@@ -1929,164 +2183,124 @@ List<Widget> createTestMenus({
void Function(TestMenu)? onOpen, void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose, void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{}, Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeStandard = false,
bool includeExtraGroups = false, bool includeExtraGroups = false,
bool accelerators = false,
}) { }) {
Widget submenuButton(
TestMenu menu, {
required List<Widget> menuChildren,
}) {
return SubmenuButton(
onOpen: onOpen != null ? () => onOpen(menu) : null,
onClose: onClose != null ? () => onClose(menu) : null,
menuChildren: menuChildren,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
Widget menuItemButton(
TestMenu menu, {
bool enabled = true,
Widget? leadingIcon,
Widget? trailingIcon,
Key? key,
}) {
return MenuItemButton(
key: key,
onPressed: enabled && onPressed != null ? () => onPressed(menu) : null,
shortcut: shortcuts[menu],
leadingIcon: leadingIcon,
trailingIcon: trailingIcon,
child: accelerators ? MenuAcceleratorLabel(menu.acceleratorLabel) : Text(menu.label),
);
}
final List<Widget> result = <Widget>[ final List<Widget> result = <Widget>[
SubmenuButton( submenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null, TestMenu.mainMenu0,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subMenu00, leadingIcon: const Icon(Icons.add)),
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null, menuItemButton(TestMenu.subMenu01),
shortcut: shortcuts[TestMenu.subMenu00], menuItemButton(TestMenu.subMenu02),
leadingIcon: const Icon(Icons.add),
child: Text(TestMenu.subMenu00.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu01) : null,
shortcut: shortcuts[TestMenu.subMenu01],
child: Text(TestMenu.subMenu01.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu02) : null,
shortcut: shortcuts[TestMenu.subMenu02],
child: Text(TestMenu.subMenu02.label),
),
], ],
child: Text(TestMenu.mainMenu0.label),
), ),
SubmenuButton( submenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null, TestMenu.mainMenu1,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subMenu10),
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null, submenuButton(
shortcut: shortcuts[TestMenu.subMenu10], TestMenu.subMenu11,
child: Text(TestMenu.subMenu10.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subSubMenu110, key: UniqueKey()),
key: UniqueKey(), menuItemButton(TestMenu.subSubMenu111),
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null, menuItemButton(TestMenu.subSubMenu112),
shortcut: shortcuts[TestMenu.subSubMenu110], menuItemButton(TestMenu.subSubMenu113),
child: Text(TestMenu.subSubMenu110.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
shortcut: shortcuts[TestMenu.subSubMenu111],
child: Text(TestMenu.subSubMenu111.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
shortcut: shortcuts[TestMenu.subSubMenu112],
child: Text(TestMenu.subSubMenu112.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
shortcut: shortcuts[TestMenu.subSubMenu113],
child: Text(TestMenu.subSubMenu113.label),
),
], ],
child: Text(TestMenu.subMenu11.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
shortcut: shortcuts[TestMenu.subMenu12],
child: Text(TestMenu.subMenu12.label),
), ),
menuItemButton(TestMenu.subMenu12),
], ],
child: Text(TestMenu.mainMenu1.label),
), ),
SubmenuButton( submenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null, TestMenu.mainMenu2,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(
// Always disabled. TestMenu.subMenu20,
leadingIcon: const Icon(Icons.ac_unit), leadingIcon: const Icon(Icons.ac_unit),
shortcut: shortcuts[TestMenu.subMenu20], enabled: false,
child: Text(TestMenu.subMenu20.label),
), ),
], ],
child: Text(TestMenu.mainMenu2.label),
), ),
if (includeExtraGroups) if (includeExtraGroups)
SubmenuButton( submenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null, TestMenu.mainMenu3,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subMenu30, enabled: false),
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu30],
// Always disabled.
child: Text(TestMenu.subMenu30.label),
),
], ],
child: Text(TestMenu.mainMenu3.label),
), ),
if (includeExtraGroups) if (includeExtraGroups)
SubmenuButton( submenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null, TestMenu.mainMenu4,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
menuChildren: <Widget>[ menuChildren: <Widget>[
MenuItemButton( menuItemButton(TestMenu.subMenu40, enabled: false),
// Always disabled. menuItemButton(TestMenu.subMenu41, enabled: false),
shortcut: shortcuts[TestMenu.subMenu40], menuItemButton(TestMenu.subMenu42, enabled: false),
// Always disabled.
child: Text(TestMenu.subMenu40.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu41],
// Always disabled.
child: Text(TestMenu.subMenu41.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu42],
// Always disabled.
child: Text(TestMenu.subMenu42.label),
),
], ],
child: Text(TestMenu.mainMenu4.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu5) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu5) : null,
menuChildren: const <Widget>[],
child: Text(TestMenu.mainMenu5.label),
), ),
submenuButton(TestMenu.mainMenu5, menuChildren: const <Widget>[]),
]; ];
return result; return result;
} }
enum TestMenu { enum TestMenu {
mainMenu0('Menu 0'), mainMenu0('&Menu 0'),
mainMenu1('Menu 1'), mainMenu1('M&enu &1'),
mainMenu2('Menu 2'), mainMenu2('Me&nu 2'),
mainMenu3('Menu 3'), mainMenu3('Men&u 3'),
mainMenu4('Menu 4'), mainMenu4('Menu &4'),
mainMenu5('Menu 5'), mainMenu5('Menu &5 && &6 &'),
subMenu00('Sub Menu 00'), subMenu00('Sub &Menu 0&0'),
subMenu01('Sub Menu 01'), subMenu01('Sub Menu 0&1'),
subMenu02('Sub Menu 02'), subMenu02('Sub Menu 0&2'),
subMenu10('Sub Menu 10'), subMenu10('Sub Menu 1&0'),
subMenu11('Sub Menu 11'), subMenu11('Sub Menu 1&1'),
subMenu12('Sub Menu 12'), subMenu12('Sub Menu 1&2'),
subMenu20('Sub Menu 20'), subMenu20('Sub Menu 2&0'),
subMenu30('Sub Menu 30'), subMenu30('Sub Menu 3&0'),
subMenu40('Sub Menu 40'), subMenu40('Sub Menu 4&0'),
subMenu41('Sub Menu 41'), subMenu41('Sub Menu 4&1'),
subMenu42('Sub Menu 42'), subMenu42('Sub Menu 4&2'),
subSubMenu110('Sub Sub Menu 110'), subSubMenu110('Sub Sub Menu 11&0'),
subSubMenu111('Sub Sub Menu 111'), subSubMenu111('Sub Sub Menu 11&1'),
subSubMenu112('Sub Sub Menu 112'), subSubMenu112('Sub Sub Menu 11&2'),
subSubMenu113('Sub Sub Menu 113'); subSubMenu113('Sub Sub Menu 11&3');
const TestMenu(this.label); const TestMenu(this.acceleratorLabel);
final String label; final String acceleratorLabel;
// Strip the accelerator markers.
String get label => MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel);
int get acceleratorIndex {
int index = -1;
MenuAcceleratorLabel.stripAcceleratorMarkers(acceleratorLabel, setIndex: (int i) => index = i);
return index;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment