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.
This commit is contained in:
parent
db631f1496
commit
0cb9f70460
@ -29,6 +29,7 @@ class _HomeState extends State<Home> {
|
||||
TextDirection _textDirection = TextDirection.ltr;
|
||||
double _extraPadding = 0;
|
||||
bool _addItem = false;
|
||||
bool _accelerators = true;
|
||||
bool _transparent = false;
|
||||
bool _funkyTheme = false;
|
||||
|
||||
@ -99,6 +100,7 @@ class _HomeState extends State<Home> {
|
||||
children: <Widget>[
|
||||
_TestMenus(
|
||||
menuController: _controller,
|
||||
accelerators: _accelerators,
|
||||
addItem: _addItem,
|
||||
),
|
||||
Expanded(
|
||||
@ -107,6 +109,7 @@ class _HomeState extends State<Home> {
|
||||
menuController: _controller,
|
||||
density: _density,
|
||||
addItem: _addItem,
|
||||
accelerators: _accelerators,
|
||||
transparent: _transparent,
|
||||
funkyTheme: _funkyTheme,
|
||||
extraPadding: _extraPadding,
|
||||
@ -131,6 +134,11 @@ class _HomeState extends State<Home> {
|
||||
_addItem = value;
|
||||
});
|
||||
},
|
||||
onAcceleratorsChanged: (bool value) {
|
||||
setState(() {
|
||||
_accelerators = value;
|
||||
});
|
||||
},
|
||||
onTransparentChanged: (bool value) {
|
||||
setState(() {
|
||||
_transparent = value;
|
||||
@ -159,12 +167,14 @@ class _Controls extends StatefulWidget {
|
||||
required this.textDirection,
|
||||
required this.extraPadding,
|
||||
this.addItem = false,
|
||||
this.accelerators = true,
|
||||
this.transparent = false,
|
||||
this.funkyTheme = false,
|
||||
required this.onDensityChanged,
|
||||
required this.onTextDirectionChanged,
|
||||
required this.onExtraPaddingChanged,
|
||||
required this.onAddItemChanged,
|
||||
required this.onAcceleratorsChanged,
|
||||
required this.onTransparentChanged,
|
||||
required this.onFunkyThemeChanged,
|
||||
required this.menuController,
|
||||
@ -174,12 +184,14 @@ class _Controls extends StatefulWidget {
|
||||
final TextDirection textDirection;
|
||||
final double extraPadding;
|
||||
final bool addItem;
|
||||
final bool accelerators;
|
||||
final bool transparent;
|
||||
final bool funkyTheme;
|
||||
final ValueChanged<VisualDensity> onDensityChanged;
|
||||
final ValueChanged<TextDirection> onTextDirectionChanged;
|
||||
final ValueChanged<double> onExtraPaddingChanged;
|
||||
final ValueChanged<bool> onAddItemChanged;
|
||||
final ValueChanged<bool> onAcceleratorsChanged;
|
||||
final ValueChanged<bool> onTransparentChanged;
|
||||
final ValueChanged<bool> onFunkyThemeChanged;
|
||||
final MenuController menuController;
|
||||
@ -199,165 +211,180 @@ class _ControlsState extends State<_Controls> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.lightBlueAccent,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
MenuAnchor(
|
||||
childFocusNode: _focusNode,
|
||||
style: const MenuStyle(alignment: AlignmentDirectional.topEnd),
|
||||
alignmentOffset: const Offset(100, -8),
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: TestMenu.standaloneMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.standaloneMenu1);
|
||||
},
|
||||
child: Text(TestMenu.standaloneMenu1.label),
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
MenuAnchor(
|
||||
childFocusNode: _focusNode,
|
||||
style: const MenuStyle(alignment: AlignmentDirectional.topEnd),
|
||||
alignmentOffset: const Offset(100, -8),
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: TestMenu.standaloneMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.standaloneMenu1);
|
||||
},
|
||||
child: MenuAcceleratorLabel(TestMenu.standaloneMenu1.label),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.send),
|
||||
trailingIcon: const Icon(Icons.mail),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.standaloneMenu2);
|
||||
},
|
||||
child: MenuAcceleratorLabel(TestMenu.standaloneMenu2.label),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return TextButton(
|
||||
focusNode: _focusNode,
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: const MenuAcceleratorLabel('Open Menu'),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
_ControlSlider(
|
||||
label: 'Extra Padding: ${widget.extraPadding.toStringAsFixed(1)}',
|
||||
value: widget.extraPadding,
|
||||
max: 40,
|
||||
divisions: 20,
|
||||
onChanged: (double value) {
|
||||
widget.onExtraPaddingChanged(value);
|
||||
},
|
||||
),
|
||||
_ControlSlider(
|
||||
label: 'Horizontal Density: ${widget.density.horizontal.toStringAsFixed(1)}',
|
||||
value: widget.density.horizontal,
|
||||
max: 4,
|
||||
min: -4,
|
||||
divisions: 12,
|
||||
onChanged: (double value) {
|
||||
widget.onDensityChanged(
|
||||
VisualDensity(
|
||||
horizontal: value,
|
||||
vertical: widget.density.vertical,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ControlSlider(
|
||||
label: 'Vertical Density: ${widget.density.vertical.toStringAsFixed(1)}',
|
||||
value: widget.density.vertical,
|
||||
max: 4,
|
||||
min: -4,
|
||||
divisions: 12,
|
||||
onChanged: (double value) {
|
||||
widget.onDensityChanged(
|
||||
VisualDensity(
|
||||
horizontal: widget.density.horizontal,
|
||||
vertical: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.send),
|
||||
trailingIcon: const Icon(Icons.mail),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.standaloneMenu2);
|
||||
},
|
||||
child: Text(TestMenu.standaloneMenu2.label),
|
||||
),
|
||||
],
|
||||
builder: (BuildContext context, MenuController controller, Widget? child) {
|
||||
return TextButton(
|
||||
focusNode: _focusNode,
|
||||
onPressed: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: const Text('Open Menu'),
|
||||
),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
_ControlSlider(
|
||||
label: 'Extra Padding: ${widget.extraPadding.toStringAsFixed(1)}',
|
||||
value: widget.extraPadding,
|
||||
max: 40,
|
||||
divisions: 20,
|
||||
onChanged: (double value) {
|
||||
widget.onExtraPaddingChanged(value);
|
||||
},
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.textDirection == TextDirection.rtl,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onTextDirectionChanged(TextDirection.rtl);
|
||||
} else {
|
||||
widget.onTextDirectionChanged(TextDirection.ltr);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('RTL Text')
|
||||
],
|
||||
),
|
||||
_ControlSlider(
|
||||
label: 'Horizontal Density: ${widget.density.horizontal.toStringAsFixed(1)}',
|
||||
value: widget.density.horizontal,
|
||||
max: 4,
|
||||
min: -4,
|
||||
divisions: 12,
|
||||
onChanged: (double value) {
|
||||
widget.onDensityChanged(
|
||||
VisualDensity(
|
||||
horizontal: value,
|
||||
vertical: widget.density.vertical,
|
||||
),
|
||||
);
|
||||
},
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.addItem,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onAddItemChanged(true);
|
||||
} else {
|
||||
widget.onAddItemChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Add Item')
|
||||
],
|
||||
),
|
||||
_ControlSlider(
|
||||
label: 'Vertical Density: ${widget.density.vertical.toStringAsFixed(1)}',
|
||||
value: widget.density.vertical,
|
||||
max: 4,
|
||||
min: -4,
|
||||
divisions: 12,
|
||||
onChanged: (double value) {
|
||||
widget.onDensityChanged(
|
||||
VisualDensity(
|
||||
horizontal: widget.density.horizontal,
|
||||
vertical: value,
|
||||
),
|
||||
);
|
||||
},
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.transparent,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onTransparentChanged(true);
|
||||
} else {
|
||||
widget.onTransparentChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Transparent')
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.funkyTheme,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onFunkyThemeChanged(true);
|
||||
} else {
|
||||
widget.onFunkyThemeChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Funky Theme')
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.textDirection == TextDirection.rtl,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onTextDirectionChanged(TextDirection.rtl);
|
||||
} else {
|
||||
widget.onTextDirectionChanged(TextDirection.ltr);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('RTL Text')
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.addItem,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onAddItemChanged(true);
|
||||
} else {
|
||||
widget.onAddItemChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Add Item')
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.transparent,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onTransparentChanged(true);
|
||||
} else {
|
||||
widget.onTransparentChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Transparent')
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Checkbox(
|
||||
value: widget.funkyTheme,
|
||||
onChanged: (bool? value) {
|
||||
if (value ?? false) {
|
||||
widget.onFunkyThemeChanged(true);
|
||||
} else {
|
||||
widget.onFunkyThemeChanged(false);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text('Funky Theme')
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -412,10 +439,12 @@ class _TestMenus extends StatefulWidget {
|
||||
const _TestMenus({
|
||||
required this.menuController,
|
||||
this.addItem = false,
|
||||
this.accelerators = false,
|
||||
});
|
||||
|
||||
final MenuController menuController;
|
||||
final bool addItem;
|
||||
final bool accelerators;
|
||||
|
||||
@override
|
||||
State<_TestMenus> createState() => _TestMenusState();
|
||||
@ -439,8 +468,8 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
debugPrint('App: Closed item ${item.label}');
|
||||
}
|
||||
|
||||
void _setRadio(TestMenu item) {
|
||||
debugPrint('App: Set Radio item ${item.label}');
|
||||
void _setRadio(TestMenu? item) {
|
||||
debugPrint('App: Set Radio item ${item?.label}');
|
||||
setState(() {
|
||||
radioValue = item;
|
||||
});
|
||||
@ -449,17 +478,17 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
void _setCheck(TestMenu item) {
|
||||
debugPrint('App: Set Checkbox item ${item.label}');
|
||||
setState(() {
|
||||
switch (checkboxState) {
|
||||
case false:
|
||||
checkboxState = true;
|
||||
break;
|
||||
case true:
|
||||
checkboxState = null;
|
||||
break;
|
||||
case null:
|
||||
checkboxState = false;
|
||||
break;
|
||||
}
|
||||
switch (checkboxState) {
|
||||
case false:
|
||||
checkboxState = true;
|
||||
break;
|
||||
case true:
|
||||
checkboxState = null;
|
||||
break;
|
||||
case null:
|
||||
checkboxState = false;
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -469,9 +498,9 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
_shortcutsEntry?.dispose();
|
||||
final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{};
|
||||
for (final TestMenu item in TestMenu.values) {
|
||||
if (item.shortcut == null) {
|
||||
if (item.shortcut == null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
switch (item) {
|
||||
case TestMenu.radioMenu1:
|
||||
case TestMenu.radioMenu2:
|
||||
@ -519,219 +548,21 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
Expanded(
|
||||
child: MenuBar(
|
||||
controller: widget.menuController,
|
||||
children: <Widget>[
|
||||
SubmenuButton(
|
||||
onOpen: () {
|
||||
_openItem(TestMenu.mainMenu1);
|
||||
},
|
||||
onClose: () {
|
||||
_closeItem(TestMenu.mainMenu1);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
CheckboxMenuButton(
|
||||
value: checkboxState,
|
||||
tristate: true,
|
||||
shortcut: TestMenu.subMenu1.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
checkboxState = value;
|
||||
});
|
||||
_itemSelected(TestMenu.subMenu1);
|
||||
},
|
||||
child: Text(TestMenu.subMenu1.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu1,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu1.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu1);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu1.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu2,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu2.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu2);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu2.label),
|
||||
),
|
||||
RadioMenuButton<TestMenu>(
|
||||
value: TestMenu.radioMenu3,
|
||||
groupValue: radioValue,
|
||||
toggleable: true,
|
||||
shortcut: TestMenu.radioMenu3.shortcut,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
onChanged: (TestMenu? value) {
|
||||
setState(() {
|
||||
radioValue = value;
|
||||
});
|
||||
_itemSelected(TestMenu.radioMenu3);
|
||||
},
|
||||
child: Text(TestMenu.radioMenu3.label),
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.send),
|
||||
trailingIcon: const Icon(Icons.mail),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subMenu2);
|
||||
},
|
||||
child: Text(TestMenu.subMenu2.label),
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu1.label),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: () {
|
||||
_openItem(TestMenu.mainMenu2);
|
||||
},
|
||||
onClose: () {
|
||||
_closeItem(TestMenu.mainMenu2);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('TEST'),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.testButton);
|
||||
widget.menuController.close();
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
shortcut: TestMenu.subMenu3.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subMenu3);
|
||||
},
|
||||
child: Text(TestMenu.subMenu3.label),
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu2.label),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: () {
|
||||
_openItem(TestMenu.mainMenu3);
|
||||
},
|
||||
onClose: () {
|
||||
_closeItem(TestMenu.mainMenu3);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
child: Text(TestMenu.subMenu8.label),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subMenu8);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu3.label),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: () {
|
||||
_openItem(TestMenu.mainMenu4);
|
||||
},
|
||||
onClose: () {
|
||||
_closeItem(TestMenu.mainMenu4);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
ActivateIntent: CallbackAction<ActivateIntent>(
|
||||
onInvoke: (ActivateIntent? intent) {
|
||||
debugPrint('Activated!');
|
||||
return;
|
||||
},
|
||||
)
|
||||
},
|
||||
child: MenuItemButton(
|
||||
onPressed: () {
|
||||
debugPrint('Activated text input item with ${textController.text} as a value.');
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textController,
|
||||
onSubmitted: (String value) {
|
||||
debugPrint('String $value submitted.');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: () {
|
||||
_openItem(TestMenu.subMenu5);
|
||||
},
|
||||
onClose: () {
|
||||
_closeItem(TestMenu.subMenu5);
|
||||
},
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
shortcut: TestMenu.subSubMenu1.shortcut,
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subSubMenu1);
|
||||
},
|
||||
child: Text(TestMenu.subSubMenu1.label),
|
||||
),
|
||||
MenuItemButton(
|
||||
child: Text(TestMenu.subSubMenu2.label),
|
||||
onPressed: () {
|
||||
_itemSelected(TestMenu.subSubMenu2);
|
||||
},
|
||||
),
|
||||
if (widget.addItem)
|
||||
SubmenuButton(
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
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),
|
||||
),
|
||||
],
|
||||
children: createTestMenus(
|
||||
onPressed: _itemSelected,
|
||||
onOpen: _openItem,
|
||||
onClose: _closeItem,
|
||||
onCheckboxChanged: (TestMenu menu, bool? value) {
|
||||
_setCheck(menu);
|
||||
},
|
||||
onRadioChanged: _setRadio,
|
||||
checkboxValue: checkboxState,
|
||||
radioValue: radioValue,
|
||||
menuController: widget.menuController,
|
||||
textEditingController: textController,
|
||||
includeExtraGroups: widget.addItem,
|
||||
accelerators: widget.accelerators,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -739,31 +570,223 @@ class _TestMenusState extends State<_TestMenus> {
|
||||
}
|
||||
}
|
||||
|
||||
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>[
|
||||
checkboxMenuButton(
|
||||
TestMenu.subMenu1,
|
||||
tristate: true,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
),
|
||||
radioMenuButton(
|
||||
TestMenu.radioMenu1,
|
||||
toggleable: true,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
),
|
||||
radioMenuButton(
|
||||
TestMenu.radioMenu2,
|
||||
toggleable: true,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
),
|
||||
radioMenuButton(
|
||||
TestMenu.radioMenu3,
|
||||
toggleable: true,
|
||||
trailingIcon: const Icon(Icons.assessment),
|
||||
),
|
||||
menuItemButton(
|
||||
TestMenu.subMenu2,
|
||||
leadingIcon: const Icon(Icons.send),
|
||||
trailingIcon: const Icon(Icons.mail),
|
||||
),
|
||||
],
|
||||
),
|
||||
submenuButton(
|
||||
TestMenu.mainMenu2,
|
||||
menuChildren: <Widget>[
|
||||
MenuAcceleratorCallbackBinding(
|
||||
onInvoke: onPressed != null
|
||||
? () {
|
||||
onPressed.call(TestMenu.testButton);
|
||||
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(TestMenu.subMenu3),
|
||||
],
|
||||
),
|
||||
submenuButton(
|
||||
TestMenu.mainMenu3,
|
||||
menuChildren: <Widget>[
|
||||
menuItemButton(TestMenu.subMenu8),
|
||||
],
|
||||
),
|
||||
submenuButton(
|
||||
TestMenu.mainMenu4,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
onPressed: () {
|
||||
debugPrint('Activated text input item with ${textEditingController?.text} as a value.');
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
controller: textEditingController,
|
||||
onSubmitted: (String value) {
|
||||
debugPrint('String $value submitted.');
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
submenuButton(
|
||||
TestMenu.subMenu5,
|
||||
menuChildren: <Widget>[
|
||||
menuItemButton(TestMenu.subSubMenu1),
|
||||
menuItemButton(TestMenu.subSubMenu2),
|
||||
if (includeExtraGroups)
|
||||
submenuButton(
|
||||
TestMenu.subSubMenu3,
|
||||
menuChildren: <Widget>[
|
||||
menuItemButton(TestMenu.subSubSubMenu1),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
menuItemButton(TestMenu.subMenu6, enabled: false),
|
||||
menuItemButton(TestMenu.subMenu7),
|
||||
menuItemButton(TestMenu.subMenu7),
|
||||
menuItemButton(TestMenu.subMenu8),
|
||||
],
|
||||
),
|
||||
];
|
||||
return result;
|
||||
}
|
||||
|
||||
enum TestMenu {
|
||||
mainMenu1('Menu 1'),
|
||||
mainMenu2('Menu 2'),
|
||||
mainMenu3('Menu 3'),
|
||||
mainMenu4('Menu 4'),
|
||||
mainMenu2('M&enu &2'),
|
||||
mainMenu3('Me&nu &3'),
|
||||
mainMenu4('Men&u &4'),
|
||||
radioMenu1('Radio Menu One', SingleActivator(LogicalKeyboardKey.digit1, control: true)),
|
||||
radioMenu2('Radio Menu Two', SingleActivator(LogicalKeyboardKey.digit2, control: true)),
|
||||
radioMenu3('Radio Menu Three', SingleActivator(LogicalKeyboardKey.digit3, control: true)),
|
||||
subMenu1('Sub Menu 1', SingleActivator(LogicalKeyboardKey.keyB, control: true)),
|
||||
subMenu2('Sub Menu 2'),
|
||||
subMenu3('Sub Menu 3', SingleActivator(LogicalKeyboardKey.enter, control: true)),
|
||||
subMenu4('Sub Menu 4'),
|
||||
subMenu5('Sub Menu 5'),
|
||||
subMenu6('Sub Menu 6', SingleActivator(LogicalKeyboardKey.tab, control: true)),
|
||||
subMenu7('Sub Menu 7'),
|
||||
subMenu8('Sub Menu 8'),
|
||||
subSubMenu1('Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f10, control: true)),
|
||||
subSubMenu2('Sub Sub Menu 2'),
|
||||
subSubMenu3('Sub Sub Menu 3'),
|
||||
subSubSubMenu1('Sub Sub Sub Menu 1', SingleActivator(LogicalKeyboardKey.f11, control: true)),
|
||||
testButton('TEST button'),
|
||||
standaloneMenu1('Standalone Menu 1', SingleActivator(LogicalKeyboardKey.keyC, control: true)),
|
||||
standaloneMenu2('Standalone Menu 2');
|
||||
subMenu1('Sub Menu &1', SingleActivator(LogicalKeyboardKey.keyB, control: true)),
|
||||
subMenu2('Sub Menu &2'),
|
||||
subMenu3('Sub Menu &3', SingleActivator(LogicalKeyboardKey.enter, control: true)),
|
||||
subMenu4('Sub Menu &4'),
|
||||
subMenu5('Sub Menu &5'),
|
||||
subMenu6('Sub Menu &6', SingleActivator(LogicalKeyboardKey.tab, control: true)),
|
||||
subMenu7('Sub Menu &7'),
|
||||
subMenu8('Sub Menu &8'),
|
||||
subSubMenu1('Sub Sub Menu &1', SingleActivator(LogicalKeyboardKey.f10, control: true)),
|
||||
subSubMenu2('Sub Sub Menu &2'),
|
||||
subSubMenu3('Sub Sub Menu &3'),
|
||||
subSubSubMenu1('Sub Sub Sub Menu &1', SingleActivator(LogicalKeyboardKey.f11, control: true)),
|
||||
testButton('&TEST && &&& Button &'),
|
||||
standaloneMenu1('Standalone Menu &1', SingleActivator(LogicalKeyboardKey.keyC, control: true)),
|
||||
standaloneMenu2('Standalone Menu &2');
|
||||
|
||||
const TestMenu(this.label, [this.shortcut]);
|
||||
final String label;
|
||||
const TestMenu(this.acceleratorLabel, [this.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;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,116 @@
|
||||
// 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 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// Flutter code sample for [MenuBar]
|
||||
/// Flutter code sample for [MenuBar].
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
@ -49,9 +49,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shortcuts(
|
||||
shortcuts: <ShortcutActivator, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const IncrementIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DecrementIntent(),
|
||||
shortcuts: const <ShortcutActivator, Intent>{
|
||||
SingleActivator(LogicalKeyboardKey.arrowUp): IncrementIntent(),
|
||||
SingleActivator(LogicalKeyboardKey.arrowDown): DecrementIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: <Type, Action<Intent>>{
|
||||
|
@ -0,0 +1,54 @@
|
||||
// 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 {
|
||||
/// A list of children containing the menu items that are the contents of the
|
||||
/// menu surrounded by this [MenuAnchor].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
final List<Widget> menuChildren;
|
||||
|
||||
/// The widget that this [MenuAnchor] surrounds.
|
||||
@ -263,7 +263,6 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
// view's edges.
|
||||
final GlobalKey _anchorKey = GlobalKey(debugLabel: kReleaseMode ? null : 'MenuAnchor');
|
||||
_MenuAnchorState? _parent;
|
||||
bool _childIsOpen = false;
|
||||
final FocusScopeNode _menuScopeNode = FocusScopeNode(debugLabel: kReleaseMode ? null : 'MenuAnchor sub menu');
|
||||
MenuController? _internalMenuController;
|
||||
final List<_MenuAnchorState> _anchorChildren = <_MenuAnchorState>[];
|
||||
@ -357,6 +356,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
return _MenuAnchorMarker(
|
||||
anchorKey: _anchorKey,
|
||||
anchor: this,
|
||||
isOpen: _isOpen,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@ -436,14 +436,12 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
return handle;
|
||||
}
|
||||
|
||||
void _childChangedOpenState(bool value) {
|
||||
if (_childIsOpen != value) {
|
||||
_parent?._childChangedOpenState(_childIsOpen || _isOpen);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_childIsOpen = value;
|
||||
});
|
||||
}
|
||||
void _childChangedOpenState() {
|
||||
if (mounted) {
|
||||
_parent?._childChangedOpenState();
|
||||
setState(() {
|
||||
// Mark dirty, but only if mounted.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,13 +481,14 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
// close it first.
|
||||
_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.
|
||||
assert(_overlayEntry == null);
|
||||
|
||||
final BuildContext outerContext = context;
|
||||
_parent?._childChangedOpenState();
|
||||
setState(() {
|
||||
_parent?._childChangedOpenState(true);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
final OverlayState overlay = Overlay.of(outerContext);
|
||||
@ -509,6 +508,7 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
// it.
|
||||
anchorKey: _anchorKey,
|
||||
anchor: this,
|
||||
isOpen: _isOpen,
|
||||
child: _Submenu(
|
||||
anchor: this,
|
||||
menuStyle: widget.style,
|
||||
@ -542,12 +542,10 @@ class _MenuAnchorState extends State<MenuAnchor> {
|
||||
_closeChildren(inDispose: inDispose);
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
if (!inDispose && mounted) {
|
||||
setState(() {
|
||||
// Notify that _isOpen may have changed state, but only if not currently
|
||||
// disposing or unmounted.
|
||||
_parent?._childChangedOpenState(false);
|
||||
});
|
||||
if (!inDispose) {
|
||||
// Notify that _childIsOpen changed state, but only if not
|
||||
// currently disposing.
|
||||
_parent?._childChangedOpenState();
|
||||
}
|
||||
widget.onClose?.call();
|
||||
}
|
||||
@ -651,11 +649,11 @@ class MenuController {
|
||||
/// 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
|
||||
/// hovering over other items will close the previous menu and open the newly
|
||||
/// hovered one. When those open/close transitions occur, [SubmenuButton.onOpen],
|
||||
/// and [SubmenuButton.onClose] are called on the corresponding [SubmenuButton] child
|
||||
/// of the menu bar.
|
||||
/// hovered one. When those open/close transitions occur,
|
||||
/// [SubmenuButton.onOpen], and [SubmenuButton.onClose] are called on the
|
||||
/// 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
|
||||
/// [CharacterActivator] assigned to them as their [MenuItemButton.shortcut],
|
||||
/// which will display an appropriate shortcut hint. Even though the shortcut
|
||||
@ -670,16 +668,17 @@ class MenuController {
|
||||
/// sure that selecting a menu item and triggering the shortcut do the same
|
||||
/// thing, it is recommended that they call the same callback.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows a [MenuBar] that contains a single top level menu,
|
||||
/// containing three items: "About", a checkbox menu item for showing a
|
||||
/// message, and "Quit". The items are identified with an enum value, and the
|
||||
/// shortcuts are registered globally with the [ShortcutRegistry].
|
||||
/// {@tool dartpad} This example shows a [MenuBar] that contains a single top
|
||||
/// level menu, containing three items: "About", a checkbox menu item for
|
||||
/// showing a message, and "Quit". The items are identified with an enum value,
|
||||
/// and the shortcuts are registered globally with the [ShortcutRegistry].
|
||||
///
|
||||
/// ** See code in examples/api/lib/material/menu_anchor/menu_bar.0.dart **
|
||||
/// {@end-tool}
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@macro flutter.material.MenuAcceleratorLabel.accelerator_sample}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [MenuAnchor], a widget that creates a region with a submenu and shows it
|
||||
@ -729,7 +728,7 @@ class MenuBar extends StatelessWidget {
|
||||
/// incorrect behaviors. Whenever the menus list is modified, a new list
|
||||
/// object must be provided.
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
@ -747,7 +746,7 @@ class MenuBar extends StatelessWidget {
|
||||
List<DiagnosticsNode> debugDescribeChildren() {
|
||||
return <DiagnosticsNode>[
|
||||
...children.map<DiagnosticsNode>(
|
||||
(Widget item) => item.toDiagnosticsNode(),
|
||||
(Widget item) => item.toDiagnosticsNode(),
|
||||
),
|
||||
];
|
||||
}
|
||||
@ -767,7 +766,7 @@ class MenuBar extends StatelessWidget {
|
||||
/// part of a [MenuBar], but may be used independently, or as part of a menu
|
||||
/// created with a [MenuAnchor].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -829,7 +828,7 @@ class MenuItemButton extends StatefulWidget {
|
||||
|
||||
/// The optional shortcut that selects this [MenuItemButton].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
@ -1029,7 +1028,7 @@ class _MenuItemButtonState extends State<MenuItemButton> {
|
||||
mergedStyle = widget.style!.merge(mergedStyle);
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
Widget child = TextButton(
|
||||
onPressed: widget.enabled ? _handleSelect : null,
|
||||
onHover: widget.enabled ? _handleHover : null,
|
||||
onFocusChange: widget.enabled ? widget.onFocusChange : null,
|
||||
@ -1045,6 +1044,15 @@ class _MenuItemButtonState extends State<MenuItemButton> {
|
||||
child: widget.child!,
|
||||
),
|
||||
);
|
||||
|
||||
if (_platformSupportsAccelerators() && widget.enabled) {
|
||||
child = MenuAcceleratorCallbackBinding(
|
||||
onInvoke: _handleSelect,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
void _handleFocusChange() {
|
||||
@ -1193,7 +1201,7 @@ class CheckboxMenuButton extends StatelessWidget {
|
||||
|
||||
/// The optional shortcut that selects this [MenuItemButton].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
@ -1390,7 +1398,7 @@ class RadioMenuButton<T> extends StatelessWidget {
|
||||
|
||||
/// The optional shortcut that selects this [MenuItemButton].
|
||||
///
|
||||
/// {@macro flutter.material.menu_bar.shortcuts_note}
|
||||
/// {@macro flutter.material.MenuBar.shortcuts_note}
|
||||
final MenuSerializableShortcut? shortcut;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
@ -1467,7 +1475,6 @@ class RadioMenuButton<T> extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A menu button that displays a cascading menu.
|
||||
///
|
||||
/// It can be used as part of a [MenuBar], or as a standalone widget.
|
||||
@ -1811,6 +1818,9 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
}
|
||||
|
||||
void toggleShowMenu(BuildContext context) {
|
||||
if (controller._anchor == null) {
|
||||
return;
|
||||
}
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
@ -1835,7 +1845,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
// 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
|
||||
// it.
|
||||
if (controller._anchor!._root._orientation == Axis.horizontal && !controller._anchor!._root._childIsOpen) {
|
||||
if (controller._anchor!._root._orientation == Axis.horizontal && !controller._anchor!._root._isOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1845,7 +1855,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
}
|
||||
}
|
||||
|
||||
return TextButton(
|
||||
child = TextButton(
|
||||
style: mergedStyle,
|
||||
focusNode: _buttonFocusNode,
|
||||
onHover: _enabled ? (bool hovering) => handleHover(hovering, context) : null,
|
||||
@ -1858,6 +1868,15 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
child: child ?? const SizedBox(),
|
||||
),
|
||||
);
|
||||
|
||||
if (_enabled && _platformSupportsAccelerators()) {
|
||||
return MenuAcceleratorCallbackBinding(
|
||||
onInvoke: () => toggleShowMenu(context),
|
||||
hasSubmenu: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
menuChildren: widget.menuChildren,
|
||||
child: widget.child,
|
||||
@ -1874,7 +1893,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
|
||||
T? resolve<T>(MaterialStateProperty<T>? Function(MenuStyle? style) getProperty) {
|
||||
return effectiveValue(
|
||||
(MenuStyle? style) {
|
||||
(MenuStyle? style) {
|
||||
return getProperty(style)?.resolve(widget.statesController?.value ?? const <MaterialState>{});
|
||||
},
|
||||
);
|
||||
@ -1882,10 +1901,7 @@ class _SubmenuButtonState extends State<SubmenuButton> {
|
||||
|
||||
return resolve<EdgeInsetsGeometry?>(
|
||||
(MenuStyle? style) => style?.padding,
|
||||
)?.resolve(
|
||||
Directionality.of(context),
|
||||
) ??
|
||||
EdgeInsets.zero;
|
||||
)?.resolve(Directionality.of(context)) ?? EdgeInsets.zero;
|
||||
}
|
||||
|
||||
void _handleFocusChange() {
|
||||
@ -2154,14 +2170,18 @@ class _MenuAnchorMarker extends InheritedWidget {
|
||||
required super.child,
|
||||
required this.anchorKey,
|
||||
required this.anchor,
|
||||
required this.isOpen,
|
||||
});
|
||||
|
||||
final GlobalKey anchorKey;
|
||||
final _MenuAnchorState anchor;
|
||||
final bool isOpen;
|
||||
|
||||
@override
|
||||
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 {
|
||||
@override
|
||||
bool get _isOpen {
|
||||
// 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
|
||||
@ -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
|
||||
/// [SubmenuButton].
|
||||
///
|
||||
@ -2857,25 +3303,25 @@ class _MenuPanelState extends State<_MenuPanel> {
|
||||
constrainedAxis: widget.orientation,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: _intrinsicCrossSize(
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
color: backgroundColor,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
type: backgroundColor == null ? MaterialType.transparency : MaterialType.canvas,
|
||||
child: _intrinsicCrossSize(
|
||||
child: Material(
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
color: backgroundColor,
|
||||
shadowColor: shadowColor,
|
||||
surfaceTintColor: surfaceTintColor,
|
||||
type: backgroundColor == null ? MaterialType.transparency : MaterialType.canvas,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: Padding(
|
||||
padding: resolvedPadding,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: widget.orientation,
|
||||
child: Flex(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
textDirection: Directionality.of(context),
|
||||
direction: widget.orientation,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
child: Padding(
|
||||
padding: resolvedPadding,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: widget.orientation,
|
||||
child: Flex(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
textDirection: Directionality.of(context),
|
||||
direction: widget.orientation,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: widget.children,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -3061,6 +3507,23 @@ bool _debugMenuInfo(String message, [Iterable<String>? details]) {
|
||||
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
|
||||
|
||||
// Do not edit by hand. The code between the "BEGIN GENERATED" and
|
||||
@ -3072,13 +3535,13 @@ bool _debugMenuInfo(String message, [Iterable<String>? details]) {
|
||||
|
||||
class _MenuBarDefaultsM3 extends MenuStyle {
|
||||
_MenuBarDefaultsM3(this.context)
|
||||
: super(
|
||||
elevation: const MaterialStatePropertyAll<double?>(3.0),
|
||||
shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
);
|
||||
: super(
|
||||
elevation: const MaterialStatePropertyAll<double?>(3.0),
|
||||
shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
|
||||
alignment: AlignmentDirectional.bottomStart,
|
||||
);
|
||||
static const RoundedRectangleBorder _defaultMenuBorder =
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
@ -3114,11 +3577,11 @@ class _MenuBarDefaultsM3 extends MenuStyle {
|
||||
|
||||
class _MenuButtonDefaultsM3 extends ButtonStyle {
|
||||
_MenuButtonDefaultsM3(this.context)
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
);
|
||||
: super(
|
||||
animationDuration: kThemeChangeDuration,
|
||||
enableFeedback: true,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
);
|
||||
final BuildContext context;
|
||||
|
||||
late final ColorScheme _colors = Theme.of(context).colorScheme;
|
||||
@ -3256,13 +3719,13 @@ class _MenuButtonDefaultsM3 extends ButtonStyle {
|
||||
|
||||
class _MenuDefaultsM3 extends MenuStyle {
|
||||
_MenuDefaultsM3(this.context)
|
||||
: super(
|
||||
elevation: const MaterialStatePropertyAll<double?>(3.0),
|
||||
shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
);
|
||||
: super(
|
||||
elevation: const MaterialStatePropertyAll<double?>(3.0),
|
||||
shape: const MaterialStatePropertyAll<OutlinedBorder>(_defaultMenuBorder),
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
);
|
||||
static const RoundedRectangleBorder _defaultMenuBorder =
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(4.0)));
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
@ -577,24 +578,43 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
|
||||
/// ** See code in examples/api/lib/widgets/shortcuts/character_activator.0.dart **
|
||||
/// {@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:
|
||||
///
|
||||
/// * [SingleActivator], an activator that represents a single key combined
|
||||
/// with modifiers, such as `Ctrl+C` or `Ctrl-Right Arrow`.
|
||||
class CharacterActivator with Diagnosticable, MenuSerializableShortcut implements ShortcutActivator {
|
||||
/// 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, {
|
||||
this.alt = false,
|
||||
this.control = false,
|
||||
@ -602,36 +622,38 @@ class CharacterActivator with Diagnosticable, MenuSerializableShortcut implement
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// or both Alt keys must be pressed.
|
||||
///
|
||||
/// one or both Alt keys must be pressed.
|
||||
///
|
||||
/// {@macro flutter.widgets.shortcuts.CharacterActivator.alt}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LogicalKeyboardKey.altLeft], [LogicalKeyboardKey.altRight].
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// either or both Control keys must be pressed.
|
||||
/// either one or both Control keys must be pressed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [LogicalKeyboardKey.controlLeft], [LogicalKeyboardKey.controlRight].
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
/// either or both Meta keys must be pressed.
|
||||
/// either one or both Meta keys must be pressed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -1150,7 +1172,7 @@ class ShortcutRegistryEntry {
|
||||
/// [ShortcutRegistryEntry] from the [registry].
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
registry._disposeToken(this);
|
||||
registry._disposeEntry(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1160,11 +1182,22 @@ class ShortcutRegistryEntry {
|
||||
/// You can reach the nearest [ShortcutRegistry] using [of] and [maybeOf].
|
||||
///
|
||||
/// 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 {
|
||||
bool _notificationScheduled = false;
|
||||
bool _disposed = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// Gets the combined shortcut bindings from all contexts that are registered
|
||||
/// with this [ShortcutRegistry], in addition to the bindings passed to
|
||||
/// [ShortcutRegistry].
|
||||
/// with this [ShortcutRegistry].
|
||||
///
|
||||
/// Listeners will be notified when the value returned by this getter changes.
|
||||
///
|
||||
@ -1172,11 +1205,12 @@ class ShortcutRegistry with ChangeNotifier {
|
||||
Map<ShortcutActivator, Intent> get shortcuts {
|
||||
assert(ChangeNotifier.debugAssertNotDisposed(this));
|
||||
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,
|
||||
};
|
||||
}
|
||||
final Map<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> _tokenShortcuts =
|
||||
|
||||
final Map<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>> _registeredShortcuts =
|
||||
<ShortcutRegistryEntry, Map<ShortcutActivator, Intent>>{};
|
||||
|
||||
/// Adds all the given shortcut bindings to this [ShortcutRegistry], and
|
||||
@ -1202,13 +1236,31 @@ class ShortcutRegistry with ChangeNotifier {
|
||||
/// shortcuts associated with a particular entry.
|
||||
ShortcutRegistryEntry addAll(Map<ShortcutActivator, Intent> value) {
|
||||
assert(ChangeNotifier.debugAssertNotDisposed(this));
|
||||
assert(value.isNotEmpty, 'Cannot register an empty map of shortcuts');
|
||||
final ShortcutRegistryEntry entry = ShortcutRegistryEntry._(this);
|
||||
_tokenShortcuts[entry] = value;
|
||||
_registeredShortcuts[entry] = value;
|
||||
assert(_debugCheckForDuplicates());
|
||||
notifyListeners();
|
||||
_notifyListenersNextFrame();
|
||||
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]
|
||||
/// which most tightly encloses the given [BuildContext].
|
||||
///
|
||||
@ -1270,23 +1322,24 @@ class ShortcutRegistry with ChangeNotifier {
|
||||
// registry.
|
||||
void _replaceAll(ShortcutRegistryEntry entry, Map<ShortcutActivator, Intent> value) {
|
||||
assert(ChangeNotifier.debugAssertNotDisposed(this));
|
||||
assert(_debugCheckTokenIsValid(entry));
|
||||
_tokenShortcuts[entry] = value;
|
||||
assert(_debugCheckEntryIsValid(entry));
|
||||
_registeredShortcuts[entry] = value;
|
||||
assert(_debugCheckForDuplicates());
|
||||
notifyListeners();
|
||||
_notifyListenersNextFrame();
|
||||
}
|
||||
|
||||
// Removes all the shortcuts associated with the given entry from this
|
||||
// registry.
|
||||
void _disposeToken(ShortcutRegistryEntry entry) {
|
||||
assert(_debugCheckTokenIsValid(entry));
|
||||
if (_tokenShortcuts.remove(entry) != null) {
|
||||
notifyListeners();
|
||||
void _disposeEntry(ShortcutRegistryEntry entry) {
|
||||
assert(_debugCheckEntryIsValid(entry));
|
||||
final Map<ShortcutActivator, Intent>? removedShortcut = _registeredShortcuts.remove(entry);
|
||||
if (removedShortcut != null) {
|
||||
_notifyListenersNextFrame();
|
||||
}
|
||||
}
|
||||
|
||||
bool _debugCheckTokenIsValid(ShortcutRegistryEntry entry) {
|
||||
if (!_tokenShortcuts.containsKey(entry)) {
|
||||
bool _debugCheckEntryIsValid(ShortcutRegistryEntry entry) {
|
||||
if (!_registeredShortcuts.containsKey(entry)) {
|
||||
if (entry.registry == this) {
|
||||
throw FlutterError('entry ${describeIdentity(entry)} is invalid.\n'
|
||||
'The entry has already been disposed of. Tokens are not valid after '
|
||||
@ -1303,7 +1356,7 @@ class ShortcutRegistry with ChangeNotifier {
|
||||
|
||||
bool _debugCheckForDuplicates() {
|
||||
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) {
|
||||
if (previous.containsKey(shortcut)) {
|
||||
throw FlutterError(
|
||||
@ -1378,10 +1431,10 @@ class _ShortcutRegistrarState extends State<ShortcutRegistrar> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Shortcuts.manager(
|
||||
manager: manager,
|
||||
child: _ShortcutRegistrarMarker(
|
||||
registry: registry,
|
||||
return _ShortcutRegistrarMarker(
|
||||
registry: registry,
|
||||
child: Shortcuts.manager(
|
||||
manager: manager,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
|
@ -191,11 +191,11 @@ void main() {
|
||||
' Localizations\n'
|
||||
' MediaQuery\n'
|
||||
' _MediaQueryFromWindow\n'
|
||||
' _ShortcutRegistrarMarker\n'
|
||||
' Semantics\n'
|
||||
' _FocusMarker\n'
|
||||
' Focus\n'
|
||||
' Shortcuts\n'
|
||||
' _ShortcutRegistrarMarker\n'
|
||||
' ShortcutRegistrar\n'
|
||||
' TapRegionSurface\n'
|
||||
' _FocusMarker\n'
|
||||
|
@ -31,7 +31,7 @@ void main() {
|
||||
}
|
||||
|
||||
void handleFocusChange() {
|
||||
focusedMenu = primaryFocus?.debugLabel ?? primaryFocus?.toString();
|
||||
focusedMenu = (primaryFocus?.debugLabel ?? primaryFocus).toString();
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
@ -392,6 +392,28 @@ void main() {
|
||||
),
|
||||
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 {
|
||||
@ -448,13 +470,16 @@ void main() {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Material(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
MenuBar(
|
||||
children: createTestMenus(onPressed: onPressed),
|
||||
),
|
||||
const Expanded(child: Placeholder()),
|
||||
],
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
MenuBar(
|
||||
children: createTestMenus(onPressed: onPressed),
|
||||
),
|
||||
const Expanded(child: Placeholder()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -463,7 +488,7 @@ void main() {
|
||||
|
||||
expect(
|
||||
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() {
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
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 {
|
||||
@ -1086,10 +1107,6 @@ void main() {
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
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 {
|
||||
@ -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', () {
|
||||
testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
@ -1605,7 +1859,7 @@ void main() {
|
||||
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[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)));
|
||||
});
|
||||
|
||||
@ -1647,7 +1901,7 @@ void main() {
|
||||
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[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)));
|
||||
});
|
||||
|
||||
@ -1687,7 +1941,7 @@ void main() {
|
||||
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[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)));
|
||||
});
|
||||
|
||||
@ -1727,7 +1981,7 @@ void main() {
|
||||
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[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)));
|
||||
});
|
||||
});
|
||||
@ -1929,164 +2183,124 @@ List<Widget> createTestMenus({
|
||||
void Function(TestMenu)? onOpen,
|
||||
void Function(TestMenu)? onClose,
|
||||
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
|
||||
bool includeStandard = 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>[
|
||||
SubmenuButton(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
|
||||
submenuButton(
|
||||
TestMenu.mainMenu0,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
|
||||
shortcut: shortcuts[TestMenu.subMenu00],
|
||||
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),
|
||||
),
|
||||
menuItemButton(TestMenu.subMenu00, leadingIcon: const Icon(Icons.add)),
|
||||
menuItemButton(TestMenu.subMenu01),
|
||||
menuItemButton(TestMenu.subMenu02),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu0.label),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
|
||||
submenuButton(
|
||||
TestMenu.mainMenu1,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
|
||||
shortcut: shortcuts[TestMenu.subMenu10],
|
||||
child: Text(TestMenu.subMenu10.label),
|
||||
),
|
||||
SubmenuButton(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
|
||||
menuItemButton(TestMenu.subMenu10),
|
||||
submenuButton(
|
||||
TestMenu.subMenu11,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
key: UniqueKey(),
|
||||
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
|
||||
shortcut: shortcuts[TestMenu.subSubMenu110],
|
||||
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),
|
||||
),
|
||||
menuItemButton(TestMenu.subSubMenu110, key: UniqueKey()),
|
||||
menuItemButton(TestMenu.subSubMenu111),
|
||||
menuItemButton(TestMenu.subSubMenu112),
|
||||
menuItemButton(TestMenu.subSubMenu113),
|
||||
],
|
||||
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(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
|
||||
submenuButton(
|
||||
TestMenu.mainMenu2,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
// Always disabled.
|
||||
menuItemButton(
|
||||
TestMenu.subMenu20,
|
||||
leadingIcon: const Icon(Icons.ac_unit),
|
||||
shortcut: shortcuts[TestMenu.subMenu20],
|
||||
child: Text(TestMenu.subMenu20.label),
|
||||
enabled: false,
|
||||
),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu2.label),
|
||||
),
|
||||
if (includeExtraGroups)
|
||||
SubmenuButton(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
|
||||
submenuButton(
|
||||
TestMenu.mainMenu3,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
// Always disabled.
|
||||
shortcut: shortcuts[TestMenu.subMenu30],
|
||||
// Always disabled.
|
||||
child: Text(TestMenu.subMenu30.label),
|
||||
),
|
||||
menuItemButton(TestMenu.subMenu30, enabled: false),
|
||||
],
|
||||
child: Text(TestMenu.mainMenu3.label),
|
||||
),
|
||||
if (includeExtraGroups)
|
||||
SubmenuButton(
|
||||
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null,
|
||||
onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
|
||||
submenuButton(
|
||||
TestMenu.mainMenu4,
|
||||
menuChildren: <Widget>[
|
||||
MenuItemButton(
|
||||
// Always disabled.
|
||||
shortcut: shortcuts[TestMenu.subMenu40],
|
||||
// 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),
|
||||
),
|
||||
menuItemButton(TestMenu.subMenu40, enabled: false),
|
||||
menuItemButton(TestMenu.subMenu41, enabled: false),
|
||||
menuItemButton(TestMenu.subMenu42, enabled: false),
|
||||
],
|
||||
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;
|
||||
}
|
||||
|
||||
enum TestMenu {
|
||||
mainMenu0('Menu 0'),
|
||||
mainMenu1('Menu 1'),
|
||||
mainMenu2('Menu 2'),
|
||||
mainMenu3('Menu 3'),
|
||||
mainMenu4('Menu 4'),
|
||||
mainMenu5('Menu 5'),
|
||||
subMenu00('Sub Menu 00'),
|
||||
subMenu01('Sub Menu 01'),
|
||||
subMenu02('Sub Menu 02'),
|
||||
subMenu10('Sub Menu 10'),
|
||||
subMenu11('Sub Menu 11'),
|
||||
subMenu12('Sub Menu 12'),
|
||||
subMenu20('Sub Menu 20'),
|
||||
subMenu30('Sub Menu 30'),
|
||||
subMenu40('Sub Menu 40'),
|
||||
subMenu41('Sub Menu 41'),
|
||||
subMenu42('Sub Menu 42'),
|
||||
subSubMenu110('Sub Sub Menu 110'),
|
||||
subSubMenu111('Sub Sub Menu 111'),
|
||||
subSubMenu112('Sub Sub Menu 112'),
|
||||
subSubMenu113('Sub Sub Menu 113');
|
||||
mainMenu0('&Menu 0'),
|
||||
mainMenu1('M&enu &1'),
|
||||
mainMenu2('Me&nu 2'),
|
||||
mainMenu3('Men&u 3'),
|
||||
mainMenu4('Menu &4'),
|
||||
mainMenu5('Menu &5 && &6 &'),
|
||||
subMenu00('Sub &Menu 0&0'),
|
||||
subMenu01('Sub Menu 0&1'),
|
||||
subMenu02('Sub Menu 0&2'),
|
||||
subMenu10('Sub Menu 1&0'),
|
||||
subMenu11('Sub Menu 1&1'),
|
||||
subMenu12('Sub Menu 1&2'),
|
||||
subMenu20('Sub Menu 2&0'),
|
||||
subMenu30('Sub Menu 3&0'),
|
||||
subMenu40('Sub Menu 4&0'),
|
||||
subMenu41('Sub Menu 4&1'),
|
||||
subMenu42('Sub Menu 4&2'),
|
||||
subSubMenu110('Sub Sub Menu 11&0'),
|
||||
subSubMenu111('Sub Sub Menu 11&1'),
|
||||
subSubMenu112('Sub Sub Menu 11&2'),
|
||||
subSubMenu113('Sub Sub Menu 11&3');
|
||||
|
||||
const TestMenu(this.label);
|
||||
final String label;
|
||||
const TestMenu(this.acceleratorLabel);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user