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