Unverified Commit 8c271e5c authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Implement Material MenuBar and MenuAnchor (#112239)

This implements a MenuBar widget that can render a Material menu bar, and a MenuAnchor widget used to create a cascading menu in a region. The menus are drawn in the overlay, while the menu bar itself is in the regular widget tree. Keyboard traversal works between the two.

This implementation of the MenuBar uses MenuAnchor to create a cascading menu that contains widgets representing the menu items. These menu items can be any kind of widget, but are typically SubmenuButtons that host submenus, or MenuItemButtons that have shortcut hints (but don't actually activate the shortcuts) and don't host submenus.

Cascading menus can be created outside of a MenuBar by using a MenuAnchor. They can be either given a specific location to appear (a coordinate), or they can be located by the MenuAnchor region that wraps the control that opens them.

The developer may also create a MenuController to pass to the various menu primitives (MenuBar or MenuAnchor) to associate menus so that they can be traversed together and closed together. Creating a controller is not required.
parent 77cb4e60
// 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';
void main() {
runApp(
const MaterialApp(
title: 'Menu Tester',
home: Material(
child: Home(),
),
),
);
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
final MenuController _controller = MenuController();
VisualDensity _density = VisualDensity.standard;
TextDirection _textDirection = TextDirection.ltr;
double _extraPadding = 0;
bool _addItem = false;
bool _transparent = false;
bool _funkyTheme = false;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
MenuThemeData menuTheme = MenuTheme.of(context);
MenuBarThemeData menuBarTheme = MenuBarTheme.of(context);
MenuButtonThemeData menuButtonTheme = MenuButtonTheme.of(context);
if (_funkyTheme) {
menuTheme = const MenuThemeData(
style: MenuStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
elevation: MaterialStatePropertyAll<double?>(10),
padding: MaterialStatePropertyAll<EdgeInsetsDirectional>(
EdgeInsetsDirectional.all(20),
),
),
);
menuButtonTheme = const MenuButtonThemeData(
style: ButtonStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
foregroundColor: MaterialStatePropertyAll<Color?>(Colors.white),
),
);
menuBarTheme = const MenuBarThemeData(
style: MenuStyle(
shape: MaterialStatePropertyAll<OutlinedBorder>(RoundedRectangleBorder()),
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
elevation: MaterialStatePropertyAll<double?>(10),
padding: MaterialStatePropertyAll<EdgeInsetsDirectional>(
EdgeInsetsDirectional.all(20),
),
),
);
}
return SafeArea(
child: Padding(
padding: EdgeInsets.all(_extraPadding),
child: Directionality(
textDirection: _textDirection,
child: Theme(
data: theme.copyWith(
visualDensity: _density,
menuTheme: _transparent
? MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(
Colors.blue.withOpacity(0.12),
),
elevation: const MaterialStatePropertyAll<double>(0),
),
)
: menuTheme,
menuBarTheme: menuBarTheme,
menuButtonTheme: menuButtonTheme,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_TestMenus(
menuController: _controller,
addItem: _addItem,
),
Expanded(
child: SingleChildScrollView(
child: _Controls(
menuController: _controller,
density: _density,
addItem: _addItem,
transparent: _transparent,
funkyTheme: _funkyTheme,
extraPadding: _extraPadding,
textDirection: _textDirection,
onDensityChanged: (VisualDensity value) {
setState(() {
_density = value;
});
},
onTextDirectionChanged: (TextDirection value) {
setState(() {
_textDirection = value;
});
},
onExtraPaddingChanged: (double value) {
setState(() {
_extraPadding = value;
});
},
onAddItemChanged: (bool value) {
setState(() {
_addItem = value;
});
},
onTransparentChanged: (bool value) {
setState(() {
_transparent = value;
});
},
onFunkyThemeChanged: (bool value) {
setState(() {
_funkyTheme = value;
});
},
),
),
),
],
),
),
),
),
);
}
}
class _Controls extends StatefulWidget {
const _Controls({
required this.density,
required this.textDirection,
required this.extraPadding,
this.addItem = false,
this.transparent = false,
this.funkyTheme = false,
required this.onDensityChanged,
required this.onTextDirectionChanged,
required this.onExtraPaddingChanged,
required this.onAddItemChanged,
required this.onTransparentChanged,
required this.onFunkyThemeChanged,
required this.menuController,
});
final VisualDensity density;
final TextDirection textDirection;
final double extraPadding;
final bool addItem;
final bool transparent;
final bool funkyTheme;
final ValueChanged<VisualDensity> onDensityChanged;
final ValueChanged<TextDirection> onTextDirectionChanged;
final ValueChanged<double> onExtraPaddingChanged;
final ValueChanged<bool> onAddItemChanged;
final ValueChanged<bool> onTransparentChanged;
final ValueChanged<bool> onFunkyThemeChanged;
final MenuController menuController;
@override
State<_Controls> createState() => _ControlsState();
}
class _ControlsState extends State<_Controls> {
final FocusNode _focusNode = FocusNode(debugLabel: 'Floating');
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.lightBlueAccent,
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MenuAnchor(
childFocusNode: _focusNode,
style: const MenuStyle(alignment: AlignmentDirectional.topEnd),
alignmentOffset: const Offset(100, -8),
menuChildren: <Widget>[
MenuItemButton(
shortcut: const SingleActivator(
LogicalKeyboardKey.keyB,
control: true,
),
onPressed: () {
_itemSelected(TestMenu.standaloneMenu1);
},
child: Text(TestMenu.standaloneMenu1.label),
),
MenuItemButton(
leadingIcon: const Icon(Icons.send),
trailingIcon: const Icon(Icons.mail),
onPressed: () {
_itemSelected(TestMenu.standaloneMenu2);
},
child: Text(TestMenu.standaloneMenu2.label),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: _focusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: child!,
);
},
child: const Text('Open Menu'),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
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,
),
);
},
),
],
),
),
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')
],
),
],
),
],
),
);
}
void _itemSelected(TestMenu item) {
debugPrint('App: Selected item ${item.label}');
}
}
class _ControlSlider extends StatelessWidget {
const _ControlSlider({
required this.label,
required this.value,
required this.onChanged,
this.min = 0,
this.max = 1,
this.divisions,
});
final String label;
final double value;
final ValueChanged<double> onChanged;
final double min;
final double max;
final int? divisions;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Container(
alignment: AlignmentDirectional.centerEnd,
constraints: const BoxConstraints(minWidth: 150),
child: Text(label),
),
Expanded(
child: Slider(
value: value,
min: min,
max: max,
divisions: divisions,
onChanged: onChanged,
),
),
],
);
}
}
class _TestMenus extends StatefulWidget {
const _TestMenus({
required this.menuController,
this.addItem = false,
});
final MenuController menuController;
final bool addItem;
@override
State<_TestMenus> createState() => _TestMenusState();
}
class _TestMenusState extends State<_TestMenus> {
final TextEditingController textController = TextEditingController();
void _itemSelected(TestMenu item) {
debugPrint('App: Selected item ${item.label}');
}
void _openItem(TestMenu item) {
debugPrint('App: Opened item ${item.label}');
}
void _closeItem(TestMenu item) {
debugPrint('App: Closed item ${item.label}');
}
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Expanded(
child: MenuBar(
controller: widget.menuController,
children: <Widget>[
SubmenuButton(
onOpen: () {
_openItem(TestMenu.mainMenu1);
},
onClose: () {
_closeItem(TestMenu.mainMenu1);
},
menuChildren: <Widget>[
MenuItemButton(
shortcut: const SingleActivator(
LogicalKeyboardKey.keyB,
control: true,
),
leadingIcon:
widget.addItem ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank),
trailingIcon: const Icon(Icons.assessment),
onPressed: () {
_itemSelected(TestMenu.subMenu1);
},
child: Text(TestMenu.subMenu1.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: const SingleActivator(
LogicalKeyboardKey.enter,
control: true,
),
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(
shortcut: const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
),
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: widget.addItem
? const SingleActivator(
LogicalKeyboardKey.f11,
control: true,
)
: const SingleActivator(
LogicalKeyboardKey.f10,
control: true,
),
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(
child: Text(TestMenu.subSubSubMenu1.label),
onPressed: () {
_itemSelected(TestMenu.subSubSubMenu1);
},
),
],
child: Text(TestMenu.subSubMenu3.label),
),
],
child: Text(TestMenu.subMenu5.label),
),
MenuItemButton(
// Disabled button
shortcut: const SingleActivator(
LogicalKeyboardKey.tab,
control: true,
),
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),
),
],
),
),
],
);
}
}
enum TestMenu {
mainMenu1('Menu 1'),
mainMenu2('Menu 2'),
mainMenu3('Menu 3'),
mainMenu4('Menu 4'),
subMenu1('Sub Menu 1'),
subMenu2('Sub Menu 2'),
subMenu3('Sub Menu 3'),
subMenu4('Sub Menu 4'),
subMenu5('Sub Menu 5'),
subMenu6('Sub Menu 6'),
subMenu7('Sub Menu 7'),
subMenu8('Sub Menu 8'),
subSubMenu1('Sub Sub Menu 1'),
subSubMenu2('Sub Sub Menu 2'),
subSubMenu3('Sub Sub Menu 3'),
subSubSubMenu1('Sub Sub Sub Menu 1'),
testButton('TEST button'),
standaloneMenu1('Standalone Menu 1'),
standaloneMenu2('Standalone Menu 2');
const TestMenu(this.label);
final String label;
}
// 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 [MenuAnchor].
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MenuApp());
/// An enhanced enum to define the available menus and their shortcuts.
///
/// Using an enum for menu definition is not required, but this illustrates how
/// they could be used for simple menu systems.
enum MenuEntry {
about('About'),
showMessage('Show Message', SingleActivator(LogicalKeyboardKey.keyS, control: true)),
hideMessage('Hide Message', SingleActivator(LogicalKeyboardKey.keyS, control: true)),
colorMenu('Color Menu'),
colorRed('Red Background', SingleActivator(LogicalKeyboardKey.keyR, control: true)),
colorGreen('Green Background', SingleActivator(LogicalKeyboardKey.keyG, control: true)),
colorBlue('Blue Background', SingleActivator(LogicalKeyboardKey.keyB, control: true));
const MenuEntry(this.label, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut;
}
class MyCascadingMenu extends StatefulWidget {
const MyCascadingMenu({super.key, required this.message});
final String message;
@override
State<MyCascadingMenu> createState() => _MyCascadingMenuState();
}
class _MyCascadingMenuState extends State<MyCascadingMenu> {
MenuEntry? _lastSelection;
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
ShortcutRegistryEntry? _shortcutsEntry;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showingMessage;
bool _showingMessage = false;
set showingMessage(bool value) {
if (_showingMessage != value) {
setState(() {
_showingMessage = value;
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Dispose of any previously registered shortcuts, since they are about to
// be replaced.
_shortcutsEntry?.dispose();
// Collect the shortcuts from the different menu selections so that they can
// be registered to apply to the entire app. Menus don't register their
// shortcuts, they only display the shortcut hint text.
final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{
for (final MenuEntry item in MenuEntry.values)
if (item.shortcut != null) item.shortcut!: VoidCallbackIntent(() => _activate(item)),
};
// Register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application.
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
@override
void dispose() {
_shortcutsEntry?.dispose();
_buttonFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
MenuAnchor(
childFocusNode: _buttonFocusNode,
menuChildren: <Widget>[
MenuItemButton(
child: Text(MenuEntry.about.label),
onPressed: () => _activate(MenuEntry.about),
),
if (_showingMessage) MenuItemButton(
onPressed: () => _activate(MenuEntry.hideMessage),
shortcut: MenuEntry.hideMessage.shortcut,
child: Text(MenuEntry.hideMessage.label),
),
if (!_showingMessage) MenuItemButton(
onPressed: () => _activate(MenuEntry.showMessage),
shortcut: MenuEntry.showMessage.shortcut,
child: Text(MenuEntry.showMessage.label),
),
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorRed),
shortcut: MenuEntry.colorRed.shortcut,
child: Text(MenuEntry.colorRed.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorGreen),
shortcut: MenuEntry.colorGreen.shortcut,
child: Text(MenuEntry.colorGreen.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorBlue),
shortcut: MenuEntry.colorBlue.shortcut,
child: Text(MenuEntry.colorBlue.label),
),
],
child: const Text('Background Color'),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return TextButton(
focusNode: _buttonFocusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('OPEN MENU'),
);
},
),
Expanded(
child: Container(
alignment: Alignment.center,
color: backgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(_lastSelection != null ? 'Last Selected: ${_lastSelection!.label}' : ''),
],
),
),
),
],
);
}
void _activate(MenuEntry selection) {
setState(() {
_lastSelection = selection;
});
switch (selection) {
case MenuEntry.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
break;
case MenuEntry.hideMessage:
case MenuEntry.showMessage:
showingMessage = !showingMessage;
break;
case MenuEntry.colorMenu:
break;
case MenuEntry.colorRed:
backgroundColor = Colors.red;
break;
case MenuEntry.colorGreen:
backgroundColor = Colors.green;
break;
case MenuEntry.colorBlue:
backgroundColor = Colors.blue;
break;
}
}
}
class MenuApp extends StatelessWidget {
const MenuApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyCascadingMenu(message: kMessage)),
);
}
}
// 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 [MenuAnchor].
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const ContextMenuApp());
/// An enhanced enum to define the available menus and their shortcuts.
///
/// Using an enum for menu definition is not required, but this illustrates how
/// they could be used for simple menu systems.
enum MenuEntry {
about('About'),
showMessage('Show Message', SingleActivator(LogicalKeyboardKey.keyS, control: true)),
hideMessage('Hide Message', SingleActivator(LogicalKeyboardKey.keyS, control: true)),
colorMenu('Color Menu'),
colorRed('Red Background', SingleActivator(LogicalKeyboardKey.keyR, control: true)),
colorGreen('Green Background', SingleActivator(LogicalKeyboardKey.keyG, control: true)),
colorBlue('Blue Background', SingleActivator(LogicalKeyboardKey.keyB, control: true));
const MenuEntry(this.label, [this.shortcut]);
final String label;
final MenuSerializableShortcut? shortcut;
}
class MyContextMenu extends StatefulWidget {
const MyContextMenu({super.key, required this.message});
final String message;
@override
State<MyContextMenu> createState() => _MyContextMenuState();
}
class _MyContextMenuState extends State<MyContextMenu> {
MenuEntry? _lastSelection;
final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button');
final MenuController _menuController = MenuController();
ShortcutRegistryEntry? _shortcutsEntry;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showingMessage;
bool _showingMessage = false;
set showingMessage(bool value) {
if (_showingMessage != value) {
setState(() {
_showingMessage = value;
});
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Dispose of any previously registered shortcuts, since they are about to
// be replaced.
_shortcutsEntry?.dispose();
// Collect the shortcuts from the different menu selections so that they can
// be registered to apply to the entire app. Menus don't register their
// shortcuts, they only display the shortcut hint text.
final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{
for (final MenuEntry item in MenuEntry.values)
if (item.shortcut != null) item.shortcut!: VoidCallbackIntent(() => _activate(item)),
};
// Register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application.
_shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts);
}
@override
void dispose() {
_shortcutsEntry?.dispose();
_buttonFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(50),
child: GestureDetector(
onTapDown: _handleTapDown,
child: MenuAnchor(
controller: _menuController,
anchorTapClosesMenu: true,
menuChildren: <Widget>[
MenuItemButton(
child: Text(MenuEntry.about.label),
onPressed: () => _activate(MenuEntry.about),
),
if (_showingMessage) MenuItemButton(
onPressed: () => _activate(MenuEntry.hideMessage),
shortcut: MenuEntry.hideMessage.shortcut,
child: Text(MenuEntry.hideMessage.label),
),
if (!_showingMessage) MenuItemButton(
onPressed: () => _activate(MenuEntry.showMessage),
shortcut: MenuEntry.showMessage.shortcut,
child: Text(MenuEntry.showMessage.label),
),
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorRed),
shortcut: MenuEntry.colorRed.shortcut,
child: Text(MenuEntry.colorRed.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorGreen),
shortcut: MenuEntry.colorGreen.shortcut,
child: Text(MenuEntry.colorGreen.label),
),
MenuItemButton(
onPressed: () => _activate(MenuEntry.colorBlue),
shortcut: MenuEntry.colorBlue.shortcut,
child: Text(MenuEntry.colorBlue.label),
),
],
child: const Text('Background Color'),
),
],
child: Container(
alignment: Alignment.center,
color: backgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('Ctrl-click anywhere on the background to show the menu.'),
),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(_lastSelection != null ? 'Last Selected: ${_lastSelection!.label}' : ''),
],
),
),
),
),
);
}
void _activate(MenuEntry selection) {
setState(() {
_lastSelection = selection;
});
switch (selection) {
case MenuEntry.about:
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
break;
case MenuEntry.showMessage:
case MenuEntry.hideMessage:
showingMessage = !showingMessage;
break;
case MenuEntry.colorMenu:
break;
case MenuEntry.colorRed:
backgroundColor = Colors.red;
break;
case MenuEntry.colorGreen:
backgroundColor = Colors.green;
break;
case MenuEntry.colorBlue:
backgroundColor = Colors.blue;
break;
}
}
void _handleTapDown(TapDownDetails details) {
if (!HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) &&
!HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlRight)) {
return;
}
_menuController.open(position: details.localPosition);
}
}
class ContextMenuApp extends StatelessWidget {
const ContextMenuApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyContextMenu(message: kMessage)),
);
}
}
// 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 [MenuBar]
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MenuBarApp());
/// A class for consolidating the definition of menu entries.
///
/// This sort of class is not required, but illustrates one way that defining
/// menus could be done.
class MenuEntry {
const MenuEntry({required this.label, this.shortcut, this.onPressed, this.menuChildren})
: assert(menuChildren == null || onPressed == null, 'onPressed is ignored if menuChildren are provided');
final String label;
final MenuSerializableShortcut? shortcut;
final VoidCallback? onPressed;
final List<MenuEntry>? menuChildren;
static List<Widget> build(List<MenuEntry> selections) {
Widget buildSelection(MenuEntry selection) {
if (selection.menuChildren != null) {
return SubmenuButton(
menuChildren: MenuEntry.build(selection.menuChildren!),
child: Text(selection.label),
);
}
return MenuItemButton(
shortcut: selection.shortcut,
onPressed: selection.onPressed,
child: Text(selection.label),
);
}
return selections.map<Widget>(buildSelection).toList();
}
static Map<MenuSerializableShortcut, Intent> shortcuts(List<MenuEntry> selections) {
final Map<MenuSerializableShortcut, Intent> result = <MenuSerializableShortcut, Intent>{};
for (final MenuEntry selection in selections) {
if (selection.menuChildren != null) {
result.addAll(MenuEntry.shortcuts(selection.menuChildren!));
} else {
if (selection.shortcut != null && selection.onPressed != null) {
result[selection.shortcut!] = VoidCallbackIntent(selection.onPressed!);
}
}
}
return result;
}
}
class MyMenuBar extends StatefulWidget {
const MyMenuBar({
super.key,
required this.message,
});
final String message;
@override
State<MyMenuBar> createState() => _MyMenuBarState();
}
class _MyMenuBarState extends State<MyMenuBar> {
ShortcutRegistryEntry? _shortcutsEntry;
String? _lastSelection;
Color get backgroundColor => _backgroundColor;
Color _backgroundColor = Colors.red;
set backgroundColor(Color value) {
if (_backgroundColor != value) {
setState(() {
_backgroundColor = value;
});
}
}
bool get showingMessage => _showMessage;
bool _showMessage = false;
set showingMessage(bool value) {
if (_showMessage != value) {
setState(() {
_showMessage = value;
});
}
}
@override
void dispose() {
_shortcutsEntry?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: MenuBar(
children: MenuEntry.build(_getMenus()),
),
),
],
),
Expanded(
child: Container(
alignment: Alignment.center,
color: backgroundColor,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
showingMessage ? widget.message : '',
style: Theme.of(context).textTheme.headlineSmall,
),
),
Text(_lastSelection != null ? 'Last Selected: $_lastSelection' : ''),
],
),
),
),
],
);
}
List<MenuEntry> _getMenus() {
final List<MenuEntry> result = <MenuEntry>[
MenuEntry(
label: 'Menu Demo',
menuChildren: <MenuEntry>[
MenuEntry(
label: 'About',
onPressed: () {
showAboutDialog(
context: context,
applicationName: 'MenuBar Sample',
applicationVersion: '1.0.0',
);
setState(() {
_lastSelection = 'About';
});
},
),
MenuEntry(
label: showingMessage ? 'Hide Message' : 'Show Message',
onPressed: () {
setState(() {
_lastSelection = showingMessage ? 'Hide Message' : 'Show Message';
showingMessage = !showingMessage;
});
},
shortcut: const SingleActivator(LogicalKeyboardKey.keyS, control: true),
),
// Hides the message, but is only enabled if the message isn't
// already hidden.
MenuEntry(
label: 'Reset Message',
onPressed: showingMessage
? () {
setState(() {
_lastSelection = 'Reset Message';
showingMessage = false;
});
}
: null,
shortcut: const SingleActivator(LogicalKeyboardKey.escape),
),
MenuEntry(
label: 'Background Color',
menuChildren: <MenuEntry>[
MenuEntry(
label: 'Red Background',
onPressed: () {
setState(() {
_lastSelection = 'Red Background';
backgroundColor = Colors.red;
});
},
shortcut: const SingleActivator(LogicalKeyboardKey.keyR, control: true),
),
MenuEntry(
label: 'Green Background',
onPressed: () {
setState(() {
_lastSelection = 'Green Background';
backgroundColor = Colors.green;
});
},
shortcut: const SingleActivator(LogicalKeyboardKey.keyG, control: true),
),
MenuEntry(
label: 'Blue Background',
onPressed: () {
setState(() {
_lastSelection = 'Blue Background';
backgroundColor = Colors.blue;
});
},
shortcut: const SingleActivator(LogicalKeyboardKey.keyB, control: true),
),
],
),
],
),
];
// (Re-)register the shortcuts with the ShortcutRegistry so that they are
// available to the entire application, and update them if they've changed.
_shortcutsEntry?.dispose();
_shortcutsEntry = ShortcutRegistry.of(context).addAll(MenuEntry.shortcuts(result));
return result;
}
}
class MenuBarApp extends StatelessWidget {
const MenuBarApp({super.key});
static const String kMessage = '"Talk less. Smile more." - A. Burr';
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: MyMenuBar(message: kMessage)),
);
}
}
// 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_anchor.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open menu', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MenuApp(),
);
await tester.tap(find.byType(TextButton));
await tester.pump();
expect(find.text(example.MenuEntry.about.label), findsOneWidget);
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text('Background Color'), findsOneWidget);
expect(find.text(example.MenuEntry.colorRed.label), findsNothing);
expect(find.text(example.MenuEntry.colorGreen.label), findsNothing);
expect(find.text(example.MenuEntry.colorBlue.label), findsNothing);
expect(find.text(example.MenuApp.kMessage), findsNothing);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.text('Background Color'), findsOneWidget);
await tester.tap(find.text('Background Color'));
await tester.pump();
expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget);
expect(find.text(example.MenuEntry.colorGreen.label), findsOneWidget);
expect(find.text(example.MenuEntry.colorBlue.label), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(find.text(example.MenuApp.kMessage), findsOneWidget);
expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget);
});
testWidgets('Shortcuts work', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MenuApp(),
);
// Open the menu so we can watch state changes resulting from the shortcuts
// firing.
await tester.tap(find.byType(TextButton));
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text(example.MenuApp.kMessage), findsNothing);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
// Need to pump twice because of the one frame delay in the notification to
// update the overlay entry.
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsNothing);
expect(find.text(example.MenuEntry.hideMessage.label), findsOneWidget);
expect(find.text(example.MenuApp.kMessage), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text(example.MenuApp.kMessage), findsNothing);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorRed.label}'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorGreen.label}'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorBlue.label}'), findsOneWidget);
});
}
// 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_anchor.1.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open menu', (WidgetTester tester) async {
Finder findMenu() {
return find.ancestor(
of: find.text(example.MenuEntry.about.label),
matching: find.byType(FocusScope),
).first;
}
await tester.pumpWidget(const example.ContextMenuApp());
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.tapAt(const Offset(100, 200));
await tester.pump();
expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(100.0, 200.0, 404.0, 352.0)));
// Make sure tapping in a different place causes the menu to move.
await tester.tapAt(const Offset(200, 100));
await tester.pump();
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight);
expect(tester.getRect(findMenu()), equals(const Rect.fromLTRB(200.0, 100.0, 504.0, 252.0)));
expect(find.text(example.MenuEntry.about.label), findsOneWidget);
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text('Background Color'), findsOneWidget);
expect(find.text(example.MenuEntry.colorRed.label), findsNothing);
expect(find.text(example.MenuEntry.colorGreen.label), findsNothing);
expect(find.text(example.MenuEntry.colorBlue.label), findsNothing);
expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.text('Background Color'), findsOneWidget);
await tester.tap(find.text('Background Color'));
await tester.pump();
expect(find.text(example.MenuEntry.colorRed.label), findsOneWidget);
expect(find.text(example.MenuEntry.colorGreen.label), findsOneWidget);
expect(find.text(example.MenuEntry.colorBlue.label), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(find.text(example.ContextMenuApp.kMessage), findsOneWidget);
expect(find.text('Last Selected: ${example.MenuEntry.showMessage.label}'), findsOneWidget);
});
testWidgets('Shortcuts work', (WidgetTester tester) async {
await tester.pumpWidget(
const example.ContextMenuApp(),
);
// Open the menu so we can look for state changes reflected in the menu.
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight);
await tester.tapAt(const Offset(100, 200));
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
// Need to pump twice because of the one frame delay in the notification to
// update the overlay entry.
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsNothing);
expect(find.text(example.MenuEntry.hideMessage.label), findsOneWidget);
expect(find.text(example.ContextMenuApp.kMessage), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
await tester.pump();
expect(find.text(example.MenuEntry.showMessage.label), findsOneWidget);
expect(find.text(example.MenuEntry.hideMessage.label), findsNothing);
expect(find.text(example.ContextMenuApp.kMessage), findsNothing);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorRed.label}'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorGreen.label}'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: ${example.MenuEntry.colorBlue.label}'), findsOneWidget);
});
}
// 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_bar.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Can open menu', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MenuBarApp(),
);
final Finder menuBarFinder = find.byType(MenuBar);
final MenuBar menuBar = tester.widget<MenuBar>(menuBarFinder);
expect(menuBar.children, isNotEmpty);
expect(menuBar.children.length, equals(1));
final Finder menuButtonFinder = find.byType(SubmenuButton).first;
await tester.tap(menuButtonFinder);
await tester.pump();
expect(find.text('About'), findsOneWidget);
expect(find.text('Show Message'), findsOneWidget);
expect(find.text('Reset Message'), findsOneWidget);
expect(find.text('Background Color'), findsOneWidget);
expect(find.text('Red Background'), findsNothing);
expect(find.text('Green Background'), findsNothing);
expect(find.text('Blue Background'), findsNothing);
expect(find.text(example.MenuBarApp.kMessage), findsNothing);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.text('About'), findsOneWidget);
expect(find.text('Show Message'), findsOneWidget);
expect(find.text('Reset Message'), findsOneWidget);
expect(find.text('Background Color'), findsOneWidget);
expect(find.text('Red Background'), findsOneWidget);
expect(find.text('Green Background'), findsOneWidget);
expect(find.text('Blue Background'), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(find.text(example.MenuBarApp.kMessage), findsOneWidget);
expect(find.text('Last Selected: Show Message'), findsOneWidget);
});
testWidgets('Shortcuts work', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MenuBarApp(),
);
expect(find.text(example.MenuBarApp.kMessage), findsNothing);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyS);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text(example.MenuBarApp.kMessage), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.text(example.MenuBarApp.kMessage), findsNothing);
expect(find.text('Last Selected: Reset Message'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyR);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: Red Background'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyG);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: Green Background'), findsOneWidget);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.keyB);
await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft);
await tester.pump();
expect(find.text('Last Selected: Blue Background'), findsOneWidget);
});
}
...@@ -111,6 +111,11 @@ export 'src/material/material_button.dart'; ...@@ -111,6 +111,11 @@ export 'src/material/material_button.dart';
export 'src/material/material_localizations.dart'; export 'src/material/material_localizations.dart';
export 'src/material/material_state.dart'; export 'src/material/material_state.dart';
export 'src/material/material_state_mixin.dart'; export 'src/material/material_state_mixin.dart';
export 'src/material/menu_anchor.dart';
export 'src/material/menu_bar_theme.dart';
export 'src/material/menu_button_theme.dart';
export 'src/material/menu_style.dart';
export 'src/material/menu_theme.dart';
export 'src/material/mergeable_material.dart'; export 'src/material/mergeable_material.dart';
export 'src/material/navigation_bar.dart'; export 'src/material/navigation_bar.dart';
export 'src/material/navigation_bar_theme.dart'; export 'src/material/navigation_bar_theme.dart';
......
This source diff could not be displayed because it is too large. You can view the blob instead.
// 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/widgets.dart';
import 'menu_anchor.dart';
import 'menu_style.dart';
import 'menu_theme.dart';
import 'theme.dart';
// Examples can assume:
// late Widget child;
/// A data class that [MenuBarTheme] uses to define the visual properties of
/// [MenuBar] widgets.
///
/// This class defines the visual properties of [MenuBar] widgets themselves,
/// but not their submenus. Those properties are defined by [MenuThemeData] or
/// [MenuButtonThemeData] instead.
///
/// Descendant widgets obtain the current [MenuBarThemeData] object using
/// `MenuBarTheme.of(context)`.
///
/// Typically, a [MenuBarThemeData] is specified as part of the overall [Theme]
/// with [ThemeData.menuBarTheme]. Otherwise, [MenuTheme] can be used to
/// configure its own widget subtree.
///
/// All [MenuBarThemeData] properties are `null` by default. If any of these
/// properties are null, the menu bar will provide its own defaults.
///
/// See also:
///
/// * [MenuThemeData], which describes the theme for the submenus of a
/// [MenuBar].
/// * [MenuButtonThemeData], which describes the theme for the [MenuItemButton]s
/// in a menu.
/// * [ThemeData], which describes the overall theme for the application.
@immutable
class MenuBarThemeData extends MenuThemeData {
/// Creates a const set of properties used to configure [MenuTheme].
const MenuBarThemeData({super.style});
/// Linearly interpolate between two text button themes.
static MenuBarThemeData? lerp(MenuBarThemeData? a, MenuBarThemeData? b, double t) {
return MenuBarThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
}
}
/// An inherited widget that defines the configuration for the [MenuBar] widgets
/// in this widget's descendants.
///
/// This class defines the visual properties of [MenuBar] widgets themselves,
/// but not their submenus. Those properties are defined by [MenuTheme] or
/// [MenuButtonTheme] instead.
///
/// Values specified here are used for [MenuBar]'s properties that are not given
/// an explicit non-null value.
///
/// See also:
/// * [MenuStyle], a configuration object that holds attributes of a menu, and
/// is used by this theme to define those attributes.
/// * [MenuTheme], which does the same thing for the menus created by a
/// [SubmenuButton] or [MenuAnchor].
/// * [MenuButtonTheme], which does the same thing for the [MenuItemButton]s
/// inside of the menus.
/// * [SubmenuButton], a button that manages a submenu that uses these
/// properties.
/// * [MenuBar], a widget that creates a menu bar that can use [SubmenuButton]s.
class MenuBarTheme extends InheritedTheme {
/// Creates a theme that controls the configurations for [MenuBar] and
/// [MenuItemButton] in its widget subtree.
const MenuBarTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// The properties to set for [MenuBar] in this widget's descendants.
final MenuBarThemeData data;
/// Returns the closest instance of this class's [data] value that encloses
/// the given context. If there is no ancestor, it returns
/// [ThemeData.menuBarTheme].
///
/// Typical usage is as follows:
///
/// ```dart
/// Widget build(BuildContext context) {
/// return MenuTheme(
/// data: const MenuThemeData(
/// style: MenuStyle(
/// backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
/// ),
/// ),
/// child: child,
/// );
/// }
/// ```
static MenuBarThemeData of(BuildContext context) {
final MenuBarTheme? menuBarTheme = context.dependOnInheritedWidgetOfExactType<MenuBarTheme>();
return menuBarTheme?.data ?? Theme.of(context).menuBarTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return MenuBarTheme(data: data, child: child);
}
@override
bool updateShouldNotify(MenuBarTheme oldWidget) => data != oldWidget.data;
}
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'material_state.dart';
import 'menu_anchor.dart';
import 'theme.dart';
// Examples can assume:
// late BuildContext context;
/// A [ButtonStyle] theme that overrides the default appearance of
/// [SubmenuButton]s and [MenuItemButton]s when it's used with a
/// [MenuButtonTheme] or with the overall [Theme]'s [ThemeData.menuTheme].
///
/// The [style]'s properties override [MenuItemButton]'s and [SubmenuButton]'s
/// default style, i.e. the [ButtonStyle] returned by
/// [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf]. Only the
/// style's non-null property values or resolved non-null
/// [MaterialStateProperty] values are used.
///
/// See also:
///
/// * [MenuButtonTheme], the theme which is configured with this class.
/// * [MenuTheme], the theme used to configure the look of the menus these
/// buttons reside in.
/// * [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf] which
/// return the default [ButtonStyle]s for menu buttons.
/// * [MenuItemButton.styleFrom] and [SubmenuButton.styleFrom], which converts
/// simple values into a [ButtonStyle] that's consistent with their respective
/// defaults.
/// * [MaterialStateProperty.resolve], "resolve" a material state property to a
/// simple value based on a set of [MaterialState]s.
/// * [ThemeData.menuButtonTheme], which can be used to override the default
/// [ButtonStyle] for [MenuItemButton]s and [SubmenuButton]s below the overall
/// [Theme].
/// * [MenuAnchor], a widget which hosts cascading menus.
/// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading
/// menus.
@immutable
class MenuButtonThemeData with Diagnosticable {
/// Creates a [MenuButtonThemeData].
///
/// The [style] may be null.
const MenuButtonThemeData({this.style});
/// Overrides for [SubmenuButton] and [MenuItemButton]'s default style.
///
/// Non-null properties or non-null resolved [MaterialStateProperty] values
/// override the [ButtonStyle] returned by [SubmenuButton.defaultStyleOf] or
/// [MenuItemButton.defaultStyleOf].
///
/// If [style] is null, then this theme doesn't override anything.
final ButtonStyle? style;
/// Linearly interpolate between two menu button themes.
static MenuButtonThemeData? lerp(MenuButtonThemeData? a, MenuButtonThemeData? b, double t) {
return MenuButtonThemeData(style: ButtonStyle.lerp(a?.style, b?.style, t));
}
@override
int get hashCode => style.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MenuButtonThemeData && other.style == style;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
}
}
/// Overrides the default [ButtonStyle] of its [MenuItemButton] and
/// [SubmenuButton] descendants.
///
/// See also:
///
/// * [MenuButtonThemeData], which is used to configure this theme.
/// * [MenuTheme], the theme used to configure the look of the menus themselves.
/// * [MenuItemButton.defaultStyleOf] and [SubmenuButton.defaultStyleOf] which
/// return the default [ButtonStyle]s for menu buttons.
/// * [MenuItemButton.styleFrom] and [SubmenuButton.styleFrom], which converts
/// simple values into a [ButtonStyle] that's consistent with their respective
/// defaults.
/// * [ThemeData.menuButtonTheme], which can be used to override the default
/// [ButtonStyle] for [MenuItemButton]s and [SubmenuButton]s below the overall
/// [Theme].
class MenuButtonTheme extends InheritedTheme {
/// Create a [MenuButtonTheme].
///
/// The [data] parameter must not be null.
const MenuButtonTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// The configuration of this theme.
final MenuButtonThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [MenuButtonTheme] widget, then
/// [ThemeData.menuButtonTheme] is used.
///
/// Typical usage is as follows:
///
/// ```dart
/// MenuButtonThemeData theme = MenuButtonTheme.of(context);
/// ```
static MenuButtonThemeData of(BuildContext context) {
final MenuButtonTheme? buttonTheme = context.dependOnInheritedWidgetOfExactType<MenuButtonTheme>();
return buttonTheme?.data ?? Theme.of(context).menuButtonTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return MenuButtonTheme(data: data, child: child);
}
@override
bool updateShouldNotify(MenuButtonTheme oldWidget) => data != oldWidget.data;
}
// 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 'dart:ui' show lerpDouble;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'button_style.dart';
import 'material_state.dart';
import 'menu_anchor.dart';
import 'theme.dart';
import 'theme_data.dart';
// Examples can assume:
// late Widget child;
// late BuildContext context;
// late MenuStyle style;
// @immutable
// class MyAppHome extends StatelessWidget {
// const MyAppHome({super.key});
// @override
// Widget build(BuildContext context) => const SizedBox();
// }
/// The visual properties that menus have in common.
///
/// Menus created by [MenuBar] and [MenuAnchor] and their themes have a
/// [MenuStyle] property which defines the visual properties whose default
/// values are to be overridden. The default values are defined by the
/// individual menu widgets and are typically based on overall theme's
/// [ThemeData.colorScheme] and [ThemeData.textTheme].
///
/// All of the [MenuStyle] properties are null by default.
///
/// Many of the [MenuStyle] properties are [MaterialStateProperty] objects which
/// resolve to different values depending on the menu's state. For example the
/// [Color] properties are defined with `MaterialStateProperty<Color>` and can
/// resolve to different colors depending on if the menu is pressed, hovered,
/// focused, disabled, etc.
///
/// These properties can override the default value for just one state or all of
/// them. For example to create a [SubmenuButton] whose background color is the
/// color scheme’s primary color with 50% opacity, but only when the menu is
/// pressed, one could write:
///
/// ```dart
/// SubmenuButton(
/// menuStyle: MenuStyle(
/// backgroundColor: MaterialStateProperty.resolveWith<Color?>(
/// (Set<MaterialState> states) {
/// if (states.contains(MaterialState.focused)) {
/// return Theme.of(context).colorScheme.primary.withOpacity(0.5);
/// }
/// return null; // Use the component's default.
/// },
/// ),
/// ),
/// menuChildren: const <Widget>[ /* ... */ ],
/// child: const Text('Fly me to the moon'),
/// ),
/// ```
///
/// In this case the background color for all other menu states would fall back
/// to the [SubmenuButton]'s default values. To unconditionally set the menu's
/// [backgroundColor] for all states one could write:
///
/// ```dart
/// const SubmenuButton(
/// menuStyle: MenuStyle(
/// backgroundColor: MaterialStatePropertyAll<Color>(Colors.green),
/// ),
/// menuChildren: <Widget>[ /* ... */ ],
/// child: Text('Let me play among the stars'),
/// ),
/// ```
///
/// To configure all of the application's menus in the same way, specify the
/// overall theme's `menuTheme`:
///
/// ```dart
/// MaterialApp(
/// theme: ThemeData(
/// menuTheme: const MenuThemeData(
/// style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.red)),
/// ),
/// ),
/// home: const MyAppHome(),
/// ),
/// ```
///
/// See also:
///
/// * [MenuAnchor], a widget which hosts cascading menus.
/// * [MenuBar], a widget which defines a menu bar of buttons hosting cascading
/// menus.
/// * [MenuButtonTheme], the theme for [SubmenuButton]s and [MenuItemButton]s.
/// * [ButtonStyle], a similar configuration object for button styles.
@immutable
class MenuStyle with Diagnosticable {
/// Create a [MenuStyle].
const MenuStyle({
this.backgroundColor,
this.shadowColor,
this.surfaceTintColor,
this.elevation,
this.padding,
this.minimumSize,
this.fixedSize,
this.maximumSize,
this.side,
this.shape,
this.mouseCursor,
this.visualDensity,
this.alignment,
});
/// The menu's background fill color.
final MaterialStateProperty<Color?>? backgroundColor;
/// The shadow color of the menu's [Material].
///
/// The material's elevation shadow can be difficult to see for dark themes,
/// so by default the menu classes add a semi-transparent overlay to indicate
/// elevation. See [ThemeData.applyElevationOverlayColor].
final MaterialStateProperty<Color?>? shadowColor;
/// The surface tint color of the menu's [Material].
///
/// See [Material.surfaceTintColor] for more details.
final MaterialStateProperty<Color?>? surfaceTintColor;
/// The elevation of the menu's [Material].
final MaterialStateProperty<double?>? elevation;
/// The padding between the menu's boundary and its child.
final MaterialStateProperty<EdgeInsetsGeometry?>? padding;
/// The minimum size of the menu itself.
///
/// This value must be less than or equal to [maximumSize].
final MaterialStateProperty<Size?>? minimumSize;
/// The menu's size.
///
/// This size is still constrained by the style's [minimumSize] and
/// [maximumSize]. Fixed size dimensions whose value is [double.infinity] are
/// ignored.
///
/// To specify menus with a fixed width and the default height use `fixedSize:
/// Size.fromWidth(320)`. Similarly, to specify a fixed height and the default
/// width use `fixedSize: Size.fromHeight(100)`.
final MaterialStateProperty<Size?>? fixedSize;
/// The maximum size of the menu itself.
///
/// A [Size.infinite] or null value for this property means that the menu's
/// maximum size is not constrained.
///
/// This value must be greater than or equal to [minimumSize].
final MaterialStateProperty<Size?>? maximumSize;
/// The color and weight of the menu's outline.
///
/// This value is combined with [shape] to create a shape decorated with an
/// outline.
final MaterialStateProperty<BorderSide?>? side;
/// The shape of the menu's underlying [Material].
///
/// This shape is combined with [side] to create a shape decorated with an
/// outline.
final MaterialStateProperty<OutlinedBorder?>? shape;
/// The cursor for a mouse pointer when it enters or is hovering over this
/// menu's [InkWell].
final MaterialStateProperty<MouseCursor?>? mouseCursor;
/// Defines how compact the menu's layout will be.
///
/// {@macro flutter.material.themedata.visualDensity}
///
/// See also:
///
/// * [ThemeData.visualDensity], which specifies the [visualDensity] for all
/// widgets within a [Theme].
final VisualDensity? visualDensity;
/// Determines the desired alignment of the submenu when opened relative to
/// the button that opens it.
///
/// If there isn't sufficient space to open the menu with the given alignment,
/// and there's space on the other side of the button, then the alignment is
/// swapped to it's opposite (1 becomes -1, etc.), and the menu will try to
/// appear on the other side of the button. If there isn't enough space there
/// either, then the menu will be pushed as far over as necessary to display
/// as much of itself as possible, possibly overlapping the parent button.
final AlignmentGeometry? alignment;
@override
int get hashCode {
final List<Object?> values = <Object?>[
backgroundColor,
shadowColor,
surfaceTintColor,
elevation,
padding,
minimumSize,
fixedSize,
maximumSize,
side,
shape,
mouseCursor,
visualDensity,
alignment,
];
return Object.hashAll(values);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MenuStyle
&& other.backgroundColor == backgroundColor
&& other.shadowColor == shadowColor
&& other.surfaceTintColor == surfaceTintColor
&& other.elevation == elevation
&& other.padding == padding
&& other.minimumSize == minimumSize
&& other.fixedSize == fixedSize
&& other.maximumSize == maximumSize
&& other.side == side
&& other.shape == shape
&& other.mouseCursor == mouseCursor
&& other.visualDensity == visualDensity
&& other.alignment == alignment;
}
/// Returns a copy of this MenuStyle with the given fields replaced with
/// the new values.
MenuStyle copyWith({
MaterialStateProperty<Color?>? backgroundColor,
MaterialStateProperty<Color?>? shadowColor,
MaterialStateProperty<Color?>? surfaceTintColor,
MaterialStateProperty<double?>? elevation,
MaterialStateProperty<EdgeInsetsGeometry?>? padding,
MaterialStateProperty<Size?>? minimumSize,
MaterialStateProperty<Size?>? fixedSize,
MaterialStateProperty<Size?>? maximumSize,
MaterialStateProperty<BorderSide?>? side,
MaterialStateProperty<OutlinedBorder?>? shape,
MaterialStateProperty<MouseCursor?>? mouseCursor,
VisualDensity? visualDensity,
AlignmentGeometry? alignment,
}) {
return MenuStyle(
backgroundColor: backgroundColor ?? this.backgroundColor,
shadowColor: shadowColor ?? this.shadowColor,
surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor,
elevation: elevation ?? this.elevation,
padding: padding ?? this.padding,
minimumSize: minimumSize ?? this.minimumSize,
fixedSize: fixedSize ?? this.fixedSize,
maximumSize: maximumSize ?? this.maximumSize,
side: side ?? this.side,
shape: shape ?? this.shape,
mouseCursor: mouseCursor ?? this.mouseCursor,
visualDensity: visualDensity ?? this.visualDensity,
alignment: alignment ?? this.alignment,
);
}
/// Returns a copy of this MenuStyle where the non-null fields in [style]
/// have replaced the corresponding null fields in this MenuStyle.
///
/// In other words, [style] is used to fill in unspecified (null) fields
/// this MenuStyle.
MenuStyle merge(MenuStyle? style) {
if (style == null) {
return this;
}
return copyWith(
backgroundColor: backgroundColor ?? style.backgroundColor,
shadowColor: shadowColor ?? style.shadowColor,
surfaceTintColor: surfaceTintColor ?? style.surfaceTintColor,
elevation: elevation ?? style.elevation,
padding: padding ?? style.padding,
minimumSize: minimumSize ?? style.minimumSize,
fixedSize: fixedSize ?? style.fixedSize,
maximumSize: maximumSize ?? style.maximumSize,
side: side ?? style.side,
shape: shape ?? style.shape,
mouseCursor: mouseCursor ?? style.mouseCursor,
visualDensity: visualDensity ?? style.visualDensity,
alignment: alignment ?? style.alignment,
);
}
/// Linearly interpolate between two [MenuStyle]s.
static MenuStyle? lerp(MenuStyle? a, MenuStyle? b, double t) {
assert (t != null);
if (a == null && b == null) {
return null;
}
return MenuStyle(
backgroundColor: MaterialStateProperty.lerp<Color?>(a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
shadowColor: MaterialStateProperty.lerp<Color?>(a?.shadowColor, b?.shadowColor, t, Color.lerp),
surfaceTintColor: MaterialStateProperty.lerp<Color?>(a?.surfaceTintColor, b?.surfaceTintColor, t, Color.lerp),
elevation: MaterialStateProperty.lerp<double?>(a?.elevation, b?.elevation, t, lerpDouble),
padding: MaterialStateProperty.lerp<EdgeInsetsGeometry?>(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp),
minimumSize: MaterialStateProperty.lerp<Size?>(a?.minimumSize, b?.minimumSize, t, Size.lerp),
fixedSize: MaterialStateProperty.lerp<Size?>(a?.fixedSize, b?.fixedSize, t, Size.lerp),
maximumSize: MaterialStateProperty.lerp<Size?>(a?.maximumSize, b?.maximumSize, t, Size.lerp),
side: _LerpSides(a?.side, b?.side, t),
shape: MaterialStateProperty.lerp<OutlinedBorder?>(a?.shape, b?.shape, t, OutlinedBorder.lerp),
mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor,
visualDensity: t < 0.5 ? a?.visualDensity : b?.visualDensity,
alignment: AlignmentGeometry.lerp(a?.alignment, b?.alignment, t),
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('backgroundColor', backgroundColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('shadowColor', shadowColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Color?>>('surfaceTintColor', surfaceTintColor, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<double?>>('elevation', elevation, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<EdgeInsetsGeometry?>>('padding', padding, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('minimumSize', minimumSize, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('fixedSize', fixedSize, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<Size?>>('maximumSize', maximumSize, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<BorderSide?>>('side', side, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<OutlinedBorder?>>('shape', shape, defaultValue: null));
properties.add(DiagnosticsProperty<MaterialStateProperty<MouseCursor?>>('mouseCursor', mouseCursor, defaultValue: null));
properties.add(DiagnosticsProperty<VisualDensity>('visualDensity', visualDensity, defaultValue: null));
properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment, defaultValue: null));
}
}
/// A required helper class because [BorderSide.lerp] doesn't support passing or
/// returning null values.
class _LerpSides implements MaterialStateProperty<BorderSide?> {
const _LerpSides(this.a, this.b, this.t);
final MaterialStateProperty<BorderSide?>? a;
final MaterialStateProperty<BorderSide?>? b;
final double t;
@override
BorderSide? resolve(Set<MaterialState> states) {
final BorderSide? resolvedA = a?.resolve(states);
final BorderSide? resolvedB = b?.resolve(states);
if (resolvedA == null && resolvedB == null) {
return null;
}
if (resolvedA == null) {
return BorderSide.lerp(BorderSide(width: 0, color: resolvedB!.color.withAlpha(0)), resolvedB, t);
}
if (resolvedB == null) {
return BorderSide.lerp(resolvedA, BorderSide(width: 0, color: resolvedA.color.withAlpha(0)), t);
}
return BorderSide.lerp(resolvedA, resolvedB, t);
}
}
// 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/foundation.dart';
import 'package:flutter/widgets.dart';
import 'menu_anchor.dart';
import 'menu_style.dart';
import 'theme.dart';
// Examples can assume:
// late Widget child;
/// Defines the configuration of the submenus created by the [SubmenuButton],
/// [MenuBar], or [MenuAnchor] widgets.
///
/// Descendant widgets obtain the current [MenuThemeData] object using
/// `MenuTheme.of(context)`.
///
/// Typically, a [MenuThemeData] is specified as part of the overall [Theme]
/// with [ThemeData.menuTheme]. Otherwise, [MenuTheme] can be used to configure
/// its own widget subtree.
///
/// All [MenuThemeData] properties are `null` by default. If any of these
/// properties are null, the menu bar will provide its own defaults.
///
/// See also:
///
/// * [ThemeData], which describes the overall theme for the application.
/// * [MenuBarThemeData], which describes the theme for the menu bar itself in a
/// [MenuBar] widget.
@immutable
class MenuThemeData with Diagnosticable {
/// Creates a const set of properties used to configure [MenuTheme].
const MenuThemeData({this.style});
/// The [MenuStyle] of a [SubmenuButton] menu.
///
/// Any values not set in the [MenuStyle] will use the menu default for that
/// property.
final MenuStyle? style;
/// Linearly interpolate between two menu button themes.
static MenuThemeData? lerp(MenuThemeData? a, MenuThemeData? b, double t) {
return MenuThemeData(style: MenuStyle.lerp(a?.style, b?.style, t));
}
@override
int get hashCode => style.hashCode;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is MenuThemeData && other.style == style;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<MenuStyle>('style', style, defaultValue: null));
}
}
/// An inherited widget that defines the configuration in this widget's
/// descendants for menus created by the [SubmenuButton], [MenuBar], or
/// [MenuAnchor] widgets.
///
/// Values specified here are used for [SubmenuButton]'s menu properties that
/// are not given an explicit non-null value.
///
/// See also:
///
/// * [MenuThemeData], a configuration object that holds attributes of a menu
/// used by this theme.
/// * [MenuBarTheme], which does the same thing for the [MenuBar] widget.
/// * [MenuBar], a widget that manages [MenuItemButton]s.
/// * [MenuAnchor], a widget that creates a region that has a submenu.
/// * [MenuItemButton], a widget that is a selectable item in a menu bar menu.
/// * [SubmenuButton], a widget that specifies an item with a cascading submenu
/// in a [MenuBar] menu.
class MenuTheme extends InheritedTheme {
/// Creates a const theme that controls the configurations for the menus
/// created by the [SubmenuButton] or [MenuAnchor] widgets.
const MenuTheme({
super.key,
required this.data,
required super.child,
}) : assert(data != null);
/// The properties for [MenuBar] and [MenuItemButton] in this widget's
/// descendants.
final MenuThemeData data;
/// Returns the closest instance of this class's [data] value that encloses
/// the given context. If there is no ancestor, it returns
/// [ThemeData.menuTheme].
///
/// Typical usage is as follows:
///
/// ```dart
/// Widget build(BuildContext context) {
/// return MenuTheme(
/// data: const MenuThemeData(
/// style: MenuStyle(
/// backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
/// ),
/// ),
/// child: child,
/// );
/// }
/// ```
static MenuThemeData of(BuildContext context) {
final MenuTheme? menuTheme = context.dependOnInheritedWidgetOfExactType<MenuTheme>();
return menuTheme?.data ?? Theme.of(context).menuTheme;
}
@override
Widget wrap(BuildContext context, Widget child) {
return MenuTheme(data: data, child: child);
}
@override
bool updateShouldNotify(MenuTheme oldWidget) => data != oldWidget.data;
}
...@@ -222,13 +222,14 @@ class TextButton extends ButtonStyleButton { ...@@ -222,13 +222,14 @@ class TextButton extends ButtonStyleButton {
/// Defines the button's default appearance. /// Defines the button's default appearance.
/// ///
/// {@template flutter.material.text_button.default_style_of}
/// The button [child]'s [Text] and [Icon] widgets are rendered with /// The button [child]'s [Text] and [Icon] widgets are rendered with
/// the [ButtonStyle]'s foreground color. The button's [InkWell] adds /// the [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered /// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material] /// or pressed. The button's background color becomes its [Material]
/// color and is transparent by default. /// color and is transparent by default.
/// ///
/// All of the ButtonStyle's defaults appear below. /// All of the [ButtonStyle]'s defaults appear below.
/// ///
/// In this list "Theme.foo" is shorthand for /// In this list "Theme.foo" is shorthand for
/// `Theme.of(context).foo`. Color scheme values like /// `Theme.of(context).foo`. Color scheme values like
...@@ -245,6 +246,7 @@ class TextButton extends ButtonStyleButton { ...@@ -245,6 +246,7 @@ class TextButton extends ButtonStyleButton {
/// ///
/// The color of the [ButtonStyle.textStyle] is not used, the /// The color of the [ButtonStyle.textStyle] is not used, the
/// [ButtonStyle.foregroundColor] color is used instead. /// [ButtonStyle.foregroundColor] color is used instead.
/// {@endtemplate}
/// ///
/// ## Material 2 defaults /// ## Material 2 defaults
/// ///
...@@ -295,6 +297,7 @@ class TextButton extends ButtonStyleButton { ...@@ -295,6 +297,7 @@ class TextButton extends ButtonStyleButton {
/// If [ThemeData.useMaterial3] is set to true the following defaults will /// If [ThemeData.useMaterial3] is set to true the following defaults will
/// be used: /// be used:
/// ///
/// {@template flutter.material.text_button.material3_defaults}
/// * `textStyle` - Theme.textTheme.labelLarge /// * `textStyle` - Theme.textTheme.labelLarge
/// * `backgroundColor` - transparent /// * `backgroundColor` - transparent
/// * `foregroundColor` /// * `foregroundColor`
...@@ -326,6 +329,7 @@ class TextButton extends ButtonStyleButton { ...@@ -326,6 +329,7 @@ class TextButton extends ButtonStyleButton {
/// * `enableFeedback` - true /// * `enableFeedback` - true
/// * `alignment` - Alignment.center /// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory /// * `splashFactory` - Theme.splashFactory
/// {@endtemplate}
@override @override
ButtonStyle defaultStyleOf(BuildContext context) { ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
......
...@@ -36,6 +36,9 @@ import 'ink_well.dart' show InteractiveInkFeatureFactory; ...@@ -36,6 +36,9 @@ import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'list_tile.dart'; import 'list_tile.dart';
import 'list_tile_theme.dart'; import 'list_tile_theme.dart';
import 'menu_bar_theme.dart';
import 'menu_button_theme.dart';
import 'menu_theme.dart';
import 'navigation_bar_theme.dart'; import 'navigation_bar_theme.dart';
import 'navigation_rail_theme.dart'; import 'navigation_rail_theme.dart';
import 'outlined_button_theme.dart'; import 'outlined_button_theme.dart';
...@@ -345,6 +348,9 @@ class ThemeData with Diagnosticable { ...@@ -345,6 +348,9 @@ class ThemeData with Diagnosticable {
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme, IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme, ListTileThemeData? listTileTheme,
MenuBarThemeData? menuBarTheme,
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme, NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme, NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme, OutlinedButtonThemeData? outlinedButtonTheme,
...@@ -568,17 +574,21 @@ class ThemeData with Diagnosticable { ...@@ -568,17 +574,21 @@ class ThemeData with Diagnosticable {
bottomSheetTheme ??= const BottomSheetThemeData(); bottomSheetTheme ??= const BottomSheetThemeData();
buttonBarTheme ??= const ButtonBarThemeData(); buttonBarTheme ??= const ButtonBarThemeData();
cardTheme ??= const CardTheme(); cardTheme ??= const CardTheme();
chipTheme ??= const ChipThemeData();
checkboxTheme ??= const CheckboxThemeData(); checkboxTheme ??= const CheckboxThemeData();
chipTheme ??= const ChipThemeData();
dataTableTheme ??= const DataTableThemeData(); dataTableTheme ??= const DataTableThemeData();
dialogTheme ??= const DialogTheme(); dialogTheme ??= const DialogTheme();
dividerTheme ??= const DividerThemeData(); dividerTheme ??= const DividerThemeData();
drawerTheme ??= const DrawerThemeData(); drawerTheme ??= const DrawerThemeData();
elevatedButtonTheme ??= const ElevatedButtonThemeData(); elevatedButtonTheme ??= const ElevatedButtonThemeData();
expansionTileTheme ??= const ExpansionTileThemeData();
filledButtonTheme ??= const FilledButtonThemeData(); filledButtonTheme ??= const FilledButtonThemeData();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData(); floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
iconButtonTheme ??= const IconButtonThemeData(); iconButtonTheme ??= const IconButtonThemeData();
listTileTheme ??= const ListTileThemeData(); listTileTheme ??= const ListTileThemeData();
menuBarTheme ??= const MenuBarThemeData();
menuButtonTheme ??= const MenuButtonThemeData();
menuTheme ??= const MenuThemeData();
navigationBarTheme ??= const NavigationBarThemeData(); navigationBarTheme ??= const NavigationBarThemeData();
navigationRailTheme ??= const NavigationRailThemeData(); navigationRailTheme ??= const NavigationRailThemeData();
outlinedButtonTheme ??= const OutlinedButtonThemeData(); outlinedButtonTheme ??= const OutlinedButtonThemeData();
...@@ -594,7 +604,6 @@ class ThemeData with Diagnosticable { ...@@ -594,7 +604,6 @@ class ThemeData with Diagnosticable {
timePickerTheme ??= const TimePickerThemeData(); timePickerTheme ??= const TimePickerThemeData();
toggleButtonsTheme ??= const ToggleButtonsThemeData(); toggleButtonsTheme ??= const ToggleButtonsThemeData();
tooltipTheme ??= const TooltipThemeData(); tooltipTheme ??= const TooltipThemeData();
expansionTileTheme ??= const ExpansionTileThemeData();
// DEPRECATED (newest deprecations at the bottom) // DEPRECATED (newest deprecations at the bottom)
accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme); accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme);
...@@ -671,6 +680,9 @@ class ThemeData with Diagnosticable { ...@@ -671,6 +680,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme, iconButtonTheme: iconButtonTheme,
listTileTheme: listTileTheme, listTileTheme: listTileTheme,
menuBarTheme: menuBarTheme,
menuButtonTheme: menuButtonTheme,
menuTheme: menuTheme,
navigationBarTheme: navigationBarTheme, navigationBarTheme: navigationBarTheme,
navigationRailTheme: navigationRailTheme, navigationRailTheme: navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme, outlinedButtonTheme: outlinedButtonTheme,
...@@ -778,6 +790,9 @@ class ThemeData with Diagnosticable { ...@@ -778,6 +790,9 @@ class ThemeData with Diagnosticable {
required this.floatingActionButtonTheme, required this.floatingActionButtonTheme,
required this.iconButtonTheme, required this.iconButtonTheme,
required this.listTileTheme, required this.listTileTheme,
required this.menuBarTheme,
required this.menuButtonTheme,
required this.menuTheme,
required this.navigationBarTheme, required this.navigationBarTheme,
required this.navigationRailTheme, required this.navigationRailTheme,
required this.outlinedButtonTheme, required this.outlinedButtonTheme,
...@@ -943,6 +958,9 @@ class ThemeData with Diagnosticable { ...@@ -943,6 +958,9 @@ class ThemeData with Diagnosticable {
assert(floatingActionButtonTheme != null), assert(floatingActionButtonTheme != null),
assert(iconButtonTheme != null), assert(iconButtonTheme != null),
assert(listTileTheme != null), assert(listTileTheme != null),
assert(menuBarTheme != null),
assert(menuButtonTheme != null),
assert(menuTheme != null),
assert(navigationBarTheme != null), assert(navigationBarTheme != null),
assert(navigationRailTheme != null), assert(navigationRailTheme != null),
assert(outlinedButtonTheme != null), assert(outlinedButtonTheme != null),
...@@ -1520,6 +1538,18 @@ class ThemeData with Diagnosticable { ...@@ -1520,6 +1538,18 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the appearance of [ListTile] widgets. /// A theme for customizing the appearance of [ListTile] widgets.
final ListTileThemeData listTileTheme; final ListTileThemeData listTileTheme;
/// A theme for customizing the color, shape, elevation, and other [MenuStyle]
/// aspects of the menu bar created by the [MenuBar] widget.
final MenuBarThemeData menuBarTheme;
/// A theme for customizing the color, shape, elevation, and text style of
/// cascading menu buttons created by [SubmenuButton] or [MenuItemButton].
final MenuButtonThemeData menuButtonTheme;
/// A theme for customizing the color, shape, elevation, and other [MenuStyle]
/// attributes of menus created by the [SubmenuButton] widget.
final MenuThemeData menuTheme;
/// A theme for customizing the background color, text style, and icon themes /// A theme for customizing the background color, text style, and icon themes
/// of a [NavigationBar]. /// of a [NavigationBar].
final NavigationBarThemeData navigationBarTheme; final NavigationBarThemeData navigationBarTheme;
...@@ -1814,6 +1844,9 @@ class ThemeData with Diagnosticable { ...@@ -1814,6 +1844,9 @@ class ThemeData with Diagnosticable {
FloatingActionButtonThemeData? floatingActionButtonTheme, FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme, IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme, ListTileThemeData? listTileTheme,
MenuBarThemeData? menuBarTheme,
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme, NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme, NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme, OutlinedButtonThemeData? outlinedButtonTheme,
...@@ -1972,6 +2005,9 @@ class ThemeData with Diagnosticable { ...@@ -1972,6 +2005,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme, floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme, iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme,
listTileTheme: listTileTheme ?? this.listTileTheme, listTileTheme: listTileTheme ?? this.listTileTheme,
menuBarTheme: menuBarTheme ?? this.menuBarTheme,
menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
menuTheme: menuTheme ?? this.menuTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme, navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme, navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme, outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
...@@ -2172,6 +2208,9 @@ class ThemeData with Diagnosticable { ...@@ -2172,6 +2208,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!, floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!, iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, t)!,
listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!, listTileTheme: ListTileThemeData.lerp(a.listTileTheme, b.listTileTheme, t)!,
menuBarTheme: MenuBarThemeData.lerp(a.menuBarTheme, b.menuBarTheme, t)!,
menuButtonTheme: MenuButtonThemeData.lerp(a.menuButtonTheme, b.menuButtonTheme, t)!,
menuTheme: MenuThemeData.lerp(a.menuTheme, b.menuTheme, t)!,
navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!, navigationBarTheme: NavigationBarThemeData.lerp(a.navigationBarTheme, b.navigationBarTheme, t)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!, navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!, outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
...@@ -2274,6 +2313,9 @@ class ThemeData with Diagnosticable { ...@@ -2274,6 +2313,9 @@ class ThemeData with Diagnosticable {
other.floatingActionButtonTheme == floatingActionButtonTheme && other.floatingActionButtonTheme == floatingActionButtonTheme &&
other.iconButtonTheme == iconButtonTheme && other.iconButtonTheme == iconButtonTheme &&
other.listTileTheme == listTileTheme && other.listTileTheme == listTileTheme &&
other.menuBarTheme == menuBarTheme &&
other.menuButtonTheme == menuButtonTheme &&
other.menuTheme == menuTheme &&
other.navigationBarTheme == navigationBarTheme && other.navigationBarTheme == navigationBarTheme &&
other.navigationRailTheme == navigationRailTheme && other.navigationRailTheme == navigationRailTheme &&
other.outlinedButtonTheme == outlinedButtonTheme && other.outlinedButtonTheme == outlinedButtonTheme &&
...@@ -2373,6 +2415,9 @@ class ThemeData with Diagnosticable { ...@@ -2373,6 +2415,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme, floatingActionButtonTheme,
iconButtonTheme, iconButtonTheme,
listTileTheme, listTileTheme,
menuBarTheme,
menuButtonTheme,
menuTheme,
navigationBarTheme, navigationBarTheme,
navigationRailTheme, navigationRailTheme,
outlinedButtonTheme, outlinedButtonTheme,
...@@ -2474,6 +2519,9 @@ class ThemeData with Diagnosticable { ...@@ -2474,6 +2519,9 @@ class ThemeData with Diagnosticable {
properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<FloatingActionButtonThemeData>('floatingActionButtonTheme', floatingActionButtonTheme, defaultValue: defaultData.floatingActionButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<IconButtonThemeData>('iconButtonTheme', iconButtonTheme, defaultValue: defaultData.iconButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<ListTileThemeData>('listTileTheme', listTileTheme, defaultValue: defaultData.listTileTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MenuBarThemeData>('menuBarTheme', menuBarTheme, defaultValue: defaultData.menuBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MenuButtonThemeData>('menuButtonTheme', menuButtonTheme, defaultValue: defaultData.menuButtonTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<MenuThemeData>('menuTheme', menuTheme, defaultValue: defaultData.menuTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<NavigationBarThemeData>('navigationBarTheme', navigationBarTheme, defaultValue: defaultData.navigationBarTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug));
...@@ -2839,7 +2887,7 @@ class VisualDensity with Diagnosticable { ...@@ -2839,7 +2887,7 @@ class VisualDensity with Diagnosticable {
Offset get baseSizeAdjustment { Offset get baseSizeAdjustment {
// The number of logical pixels represented by an increase or decrease in // The number of logical pixels represented by an increase or decrease in
// density by one. The Material Design guidelines say to increment/decrement // density by one. The Material Design guidelines say to increment/decrement
// sized in terms of four pixel increments. // sizes in terms of four pixel increments.
const double interval = 4.0; const double interval = 4.0;
return Offset(horizontal, vertical) * interval; return Offset(horizontal, vertical) * interval;
......
...@@ -2419,7 +2419,9 @@ class LayerLink { ...@@ -2419,7 +2419,9 @@ class LayerLink {
Size? leaderSize; Size? leaderSize;
@override @override
String toString() => '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })'; String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
return '${describeIdentity(this)}(${ _leader != null ? "<linked>" : "<dangling>" })';
}
} }
/// A composited layer that can be followed by a [FollowerLayer]. /// A composited layer that can be followed by a [FollowerLayer].
......
...@@ -4996,7 +4996,7 @@ class RenderFollowerLayer extends RenderProxyBox { ...@@ -4996,7 +4996,7 @@ class RenderFollowerLayer extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final Size? leaderSize = link.leaderSize; final Size? leaderSize = link.leaderSize;
assert( assert(
link.leaderSize != null || (link.leader == null || leaderAnchor == Alignment.topLeft), link.leaderSize != null || link.leader == null || leaderAnchor == Alignment.topLeft,
'$link: layer is linked to ${link.leader} but a valid leaderSize is not set. ' '$link: layer is linked to ${link.leader} but a valid leaderSize is not set. '
'leaderSize is required when leaderAnchor is not Alignment.topLeft ' 'leaderSize is required when leaderAnchor is not Alignment.topLeft '
'(current value is $leaderAnchor).', '(current value is $leaderAnchor).',
......
...@@ -85,7 +85,7 @@ export 'package:flutter/services.dart' show ...@@ -85,7 +85,7 @@ export 'package:flutter/services.dart' show
/// infrequently change. This provides a performance tradeoff where building /// infrequently change. This provides a performance tradeoff where building
/// the [Widget]s is faster but performing updates is slower. /// the [Widget]s is faster but performing updates is slower.
/// ///
/// | | _UbiquitiousInheritedElement | InheritedElement | /// | | _UbiquitousInheritedElement | InheritedElement |
/// |---------------------|------------------------------|------------------| /// |---------------------|------------------------------|------------------|
/// | insert (best case) | O(1) | O(1) | /// | insert (best case) | O(1) | O(1) |
/// | insert (worst case) | O(1) | O(n) | /// | insert (worst case) | O(1) | O(n) |
......
// 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late MenuController controller;
String? focusedMenu;
final List<TestMenu> selected = <TestMenu>[];
final List<TestMenu> opened = <TestMenu>[];
final List<TestMenu> closed = <TestMenu>[];
final GlobalKey menuItemKey = GlobalKey();
void onPressed(TestMenu item) {
selected.add(item);
}
void onOpen(TestMenu item) {
opened.add(item);
}
void onClose(TestMenu item) {
closed.add(item);
}
void handleFocusChange() {
focusedMenu = primaryFocus?.debugLabel ?? primaryFocus?.toString();
}
setUp(() {
focusedMenu = null;
selected.clear();
opened.clear();
closed.clear();
controller = MenuController();
focusedMenu = null;
});
void listenForFocusChanges() {
FocusManager.instance.addListener(handleFocusChange);
addTearDown(() => FocusManager.instance.removeListener(handleFocusChange));
}
Finder findMenuPanels() {
return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
}
Finder findMenuBarItemLabels() {
return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuItemLabel');
}
// Finds the mnemonic associated with the menu item that has the given label.
Finder findMnemonic(String label) {
return find
.descendant(
of: find.ancestor(of: find.text(label), matching: findMenuBarItemLabels()),
matching: find.byType(Text),
)
.last;
}
Widget buildTestApp({
AlignmentGeometry? alignment,
Offset alignmentOffset = Offset.zero,
TextDirection textDirection = TextDirection.ltr,
}) {
final FocusNode focusNode = FocusNode();
return MaterialApp(
home: Material(
child: Directionality(
textDirection: textDirection,
child: Center(
child: MenuAnchor(
childFocusNode: focusNode,
controller: controller,
alignmentOffset: alignmentOffset,
style: MenuStyle(alignment: alignment),
menuChildren: <Widget>[
MenuItemButton(
key: menuItemKey,
shortcut: const SingleActivator(
LogicalKeyboardKey.keyB,
control: true,
),
onPressed: () {},
child: Text(TestMenu.subMenu00.label),
),
MenuItemButton(
leadingIcon: const Icon(Icons.send),
trailingIcon: const Icon(Icons.mail),
onPressed: () {},
child: Text(TestMenu.subMenu01.label),
),
],
builder: (BuildContext context, MenuController controller, Widget? child) {
return ElevatedButton(
focusNode: focusNode,
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: child,
);
},
child: const Text('Press Me'),
),
),
),
),
);
}
Future<TestGesture> hoverOver(WidgetTester tester, Finder finder) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.moveTo(tester.getCenter(finder));
await tester.pumpAndSettle();
return gesture;
}
Material getMenuBarMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first,
);
}
group('Menu functions', () {
testWidgets('basic menu structure', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
);
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsNothing);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(opened, isEmpty);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(find.text(TestMenu.subSubMenu111.label), findsNothing);
expect(find.text(TestMenu.subSubMenu112.label), findsNothing);
expect(opened.last, equals(TestMenu.mainMenu1));
opened.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.text(TestMenu.mainMenu0.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu1.label), findsOneWidget);
expect(find.text(TestMenu.mainMenu2.label), findsOneWidget);
expect(find.text(TestMenu.subMenu10.label), findsOneWidget);
expect(find.text(TestMenu.subMenu11.label), findsOneWidget);
expect(find.text(TestMenu.subMenu12.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu110.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu111.label), findsOneWidget);
expect(find.text(TestMenu.subSubMenu112.label), findsOneWidget);
expect(opened.last, equals(TestMenu.subMenu11));
});
testWidgets('geometry', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(112.0, 69.0, 266.0, 83.0)),
);
expect(
tester.getRect(
find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1),
),
equals(const Rect.fromLTRB(104.0, 48.0, 334.0, 200.0)),
);
});
testWidgets('geometry with RTL direction', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(534.0, 69.0, 688.0, 83.0)),
);
expect(
tester.getRect(
find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1),
),
equals(const Rect.fromLTRB(466.0, 48.0, 696.0, 200.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(0, 0, 800, 48)));
// 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(246.0, 0.0, 554.0, 48.0)),
);
});
testWidgets('menu alignment and offset in LTR', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp());
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
final Finder findMenuScope = find.ancestor(of: find.byKey(menuItemKey), matching: find.byType(FocusScope)).first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 324.0, 618.0, 428.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(328.0, 276.0, 618.0, 380.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.center));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(400.0, 300.0, 690.0, 404.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.bottomEnd));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(472.0, 324.0, 762.0, 428.0)));
await tester.pumpWidget(buildTestApp(alignment: AlignmentDirectional.topStart));
await tester.pump();
final Rect menuRect = tester.getRect(findMenuScope);
await tester.pumpWidget(
buildTestApp(
alignment: AlignmentDirectional.topStart,
alignmentOffset: const Offset(10, 20),
),
);
await tester.pump();
final Rect offsetMenuRect = tester.getRect(findMenuScope);
expect(
offsetMenuRect.topLeft - menuRect.topLeft,
equals(const Offset(10, 20)),
);
});
testWidgets('menu alignment and offset in RTL', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl));
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
final Finder findMenuScope =
find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(182.0, 324.0, 472.0, 428.0)));
await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(182.0, 276.0, 472.0, 380.0)));
await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.center));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(110.0, 300.0, 400.0, 404.0)));
await tester
.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.bottomEnd));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(38.0, 324.0, 328.0, 428.0)));
await tester.pumpWidget(buildTestApp(textDirection: TextDirection.rtl, alignment: AlignmentDirectional.topStart));
await tester.pump();
final Rect menuRect = tester.getRect(findMenuScope);
await tester.pumpWidget(
buildTestApp(
textDirection: TextDirection.rtl,
alignment: AlignmentDirectional.topStart,
alignmentOffset: const Offset(10, 20),
),
);
await tester.pump();
expect(tester.getRect(findMenuScope).topLeft - menuRect.topLeft, equals(const Offset(-10, 20)));
});
testWidgets('menu position in LTR', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp(alignmentOffset: const Offset(100, 50)));
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
final Finder findMenuScope =
find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(428.0, 374.0, 718.0, 478.0)));
// Now move the menu by calling open() again with a local position on the
// anchor.
controller.open(position: const Offset(200, 200));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(510.0, 476.0, 800.0, 580.0)));
});
testWidgets('menu position in RTL', (WidgetTester tester) async {
await tester.pumpWidget(buildTestApp(
alignmentOffset: const Offset(100, 50),
textDirection: TextDirection.rtl,
));
final Rect buttonRect = tester.getRect(find.byType(ElevatedButton));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
expect(buttonRect, equals(const Rect.fromLTRB(328.0, 276.0, 472.0, 324.0)));
final Finder findMenuScope =
find.ancestor(of: find.text(TestMenu.subMenu00.label), matching: find.byType(FocusScope)).first;
// Open the menu and make sure things are the right size, in the right place.
await tester.tap(find.text('Press Me'));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(82.0, 374.0, 372.0, 478.0)));
// Now move the menu by calling open() again with a local position on the
// anchor.
controller.open(position: const Offset(400, 200));
await tester.pump();
expect(tester.getRect(findMenuScope), equals(const Rect.fromLTRB(510.0, 476.0, 800.0, 580.0)));
});
testWidgets('works with Padding around menu and overlay', (WidgetTester tester) async {
await tester.pumpWidget(
Padding(
padding: const EdgeInsets.all(10.0),
child: MaterialApp(
home: Material(
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(134.0, 91.0, 288.0, 105.0)),
);
expect(
tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
equals(const Rect.fromLTRB(126.0, 70.0, 356.0, 222.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
});
testWidgets('works with Padding around menu and overlay with RTL direction', (WidgetTester tester) async {
await tester.pumpWidget(
Padding(
padding: const EdgeInsets.all(10.0),
child: MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.rtl,
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
),
const Expanded(child: Placeholder()),
],
),
),
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(512.0, 91.0, 666.0, 105.0)),
);
expect(
tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
equals(const Rect.fromLTRB(444.0, 70.0, 674.0, 222.0)),
);
// Close and make sure it goes back where it was.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(22.0, 22.0, 778.0, 70.0)));
});
testWidgets('visual attributes can be set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
style: MenuStyle(
elevation: MaterialStateProperty.all<double?>(10),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
),
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
expect(tester.getRect(findMenuPanels()), equals(const Rect.fromLTRB(0.0, 0.0, 800.0, 48.0)));
final Material material = getMenuBarMaterial(tester);
expect(material.elevation, equals(10));
expect(material.color, equals(Colors.red));
});
testWidgets('open and close works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
);
expect(opened, isEmpty);
expect(closed, isEmpty);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1]));
expect(closed, isEmpty);
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.subMenu11]));
expect(closed, isEmpty);
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, isEmpty);
expect(closed, equals(<TestMenu>[TestMenu.subMenu11]));
opened.clear();
closed.clear();
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu0]));
expect(closed, equals(<TestMenu>[TestMenu.mainMenu1]));
});
testWidgets('select works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, equals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
opened.clear();
await tester.tap(find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(selected, equals(<TestMenu>[TestMenu.subSubMenu110]));
// Selecting a non-submenu item should close all the menus.
expect(opened, isEmpty);
expect(find.text(TestMenu.subSubMenu110.label), findsNothing);
expect(find.text(TestMenu.subMenu11.label), findsNothing);
});
testWidgets('diagnostics', (WidgetTester tester) async {
const MenuItemButton item = MenuItemButton(
shortcut: SingleActivator(LogicalKeyboardKey.keyA),
child: Text('label2'),
);
final MenuBar menuBar = MenuBar(
controller: controller,
style: const MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(10.0),
),
children: const <Widget>[item],
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: menuBar,
),
),
);
await tester.pump();
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
menuBar.debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(
description.join('\n'),
equalsIgnoringHashCodes(
'style: MenuStyle#00000(backgroundColor: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xfff44336))), elevation: MaterialStatePropertyAll(10.0))\n'
'clipBehavior: Clip.none'),
);
});
testWidgets('keyboard tab traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pumpAndSettle();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
opened.clear();
closed.clear();
// Test closing a menu with enter.
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(opened, isEmpty);
expect(closed, <TestMenu>[TestMenu.mainMenu0]);
});
testWidgets('keyboard directional traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Go back, close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Move up, should close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
// Move down, should reopen the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
});
testWidgets('keyboard directional traversal works in RTL mode', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
),
);
listenForFocusChanges();
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 12"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
// Go back, close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Move up, should close the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
// Move down, should reopen the submenu.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
// Open the next submenu again.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 111"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 112"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 113"))'));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
});
testWidgets('hover traversal works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
);
listenForFocusChanges();
// Hovering when the menu is not yet open does nothing.
await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, isNull);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
// Hovering when the menu is already open does nothing.
await hoverOver(tester, find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 0"))'));
// Hovering over the other main menu items opens them now.
await hoverOver(tester, find.text(TestMenu.mainMenu2.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 2"))'));
await hoverOver(tester, find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Menu 1"))'));
// Hovering over the menu items focuses them.
await hoverOver(tester, find.text(TestMenu.subMenu10.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Menu 10"))'));
await hoverOver(tester, find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(focusedMenu, equals('SubmenuButton(Text("Sub Menu 11"))'));
await hoverOver(tester, find.text(TestMenu.subSubMenu110.label));
await tester.pump();
expect(focusedMenu, equals('MenuItemButton(Text("Sub Sub Menu 110"))'));
});
testWidgets('menus close on ancestor scroll', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: SingleChildScrollView(
controller: scrollController,
child: Container(
height: 1000,
alignment: Alignment.center,
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, isNotEmpty);
expect(closed, isEmpty);
opened.clear();
scrollController.jumpTo(1000);
await tester.pump();
expect(opened, isEmpty);
expect(closed, isNotEmpty);
});
testWidgets('menus close on view size change', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final MediaQueryData mediaQueryData = MediaQueryData.fromWindow(tester.binding.window);
Widget build(Size size) {
return MaterialApp(
home: Material(
child: MediaQuery(
data: mediaQueryData.copyWith(size: size),
child: SingleChildScrollView(
controller: scrollController,
child: Container(
height: 1000,
alignment: Alignment.center,
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
),
),
),
),
),
),
);
}
await tester.pumpWidget(build(mediaQueryData.size));
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(opened, isNotEmpty);
expect(closed, isEmpty);
opened.clear();
const Size smallSize = Size(200, 200);
await tester.binding.setSurfaceSize(smallSize);
await tester.pumpWidget(build(smallSize));
await tester.pump();
expect(opened, isEmpty);
expect(closed, isNotEmpty);
// Reset binding when done.
await tester.binding.setSurfaceSize(mediaQueryData.size);
});
});
group('MenuController', () {
testWidgets('Moving a controller to a new instance works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
// Now pump a new menu with a different UniqueKey to dispose of the opened
// menu's node, but keep the existing controller.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
key: UniqueKey(),
controller: controller,
children: createTestMenus(
includeExtraGroups: true,
),
),
),
),
);
await tester.pumpAndSettle();
});
testWidgets('closing via controller works', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
onPressed: onPressed,
onOpen: onOpen,
onClose: onClose,
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
)
},
),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(opened, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
opened.clear();
closed.clear();
// Close menus using the controller
controller.close();
await tester.pump();
// The menu should go away,
expect(closed, unorderedEquals(<TestMenu>[TestMenu.mainMenu1, TestMenu.subMenu11]));
expect(opened, isEmpty);
});
});
group('MenuItemButton', () {
testWidgets('Shortcut mnemonics are displayed', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.keyA, control: true),
TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.keyB, shift: true),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.keyC, alt: true),
TestMenu.subSubMenu113: const SingleActivator(LogicalKeyboardKey.keyD, meta: true),
},
),
),
),
),
);
// Open a menu initially.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
Text mnemonic0;
Text mnemonic1;
Text mnemonic2;
Text mnemonic3;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('Ctrl A'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('⇧ B'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals('Alt C'));
mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
expect(mnemonic3.data, equals('Meta D'));
break;
case TargetPlatform.windows:
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('Ctrl A'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('⇧ B'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals('Alt C'));
mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
expect(mnemonic3.data, equals('Win D'));
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('⌃ A'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('⇧ B'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals('⌥ C'));
mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
expect(mnemonic3.data, equals('⌘ D'));
break;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
includeExtraGroups: true,
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.arrowRight),
TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.arrowLeft),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.arrowUp),
TestMenu.subSubMenu113: const SingleActivator(LogicalKeyboardKey.arrowDown),
},
),
),
),
),
);
await tester.pumpAndSettle();
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('→'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('←'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals('↑'));
mnemonic3 = tester.widget(findMnemonic(TestMenu.subSubMenu113.label));
expect(mnemonic3.data, equals('↓'));
// Try some weirder ones.
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: createTestMenus(
shortcuts: <TestMenu, MenuSerializableShortcut>{
TestMenu.subSubMenu110: const SingleActivator(LogicalKeyboardKey.escape),
TestMenu.subSubMenu111: const SingleActivator(LogicalKeyboardKey.fn),
TestMenu.subSubMenu112: const SingleActivator(LogicalKeyboardKey.enter),
},
),
),
),
),
);
await tester.pumpAndSettle();
mnemonic0 = tester.widget(findMnemonic(TestMenu.subSubMenu110.label));
expect(mnemonic0.data, equals('Esc'));
mnemonic1 = tester.widget(findMnemonic(TestMenu.subSubMenu111.label));
expect(mnemonic1.data, equals('Fn'));
mnemonic2 = tester.widget(findMnemonic(TestMenu.subSubMenu112.label));
expect(mnemonic2.data, equals('↵'));
}, variant: TargetPlatformVariant.all());
testWidgets('leadingIcon is used when set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
leadingIcon: const Text('leadingIcon'),
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text('leadingIcon'), findsOneWidget);
});
testWidgets('trailingIcon is used when set', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
trailingIcon: const Text('trailingIcon'),
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text('trailingIcon'), findsOneWidget);
});
testWidgets('diagnostics', (WidgetTester tester) async {
final ButtonStyle style = ButtonStyle(
shape: MaterialStateProperty.all<OutlinedBorder?>(const StadiumBorder()),
elevation: MaterialStateProperty.all<double?>(10.0),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.red),
);
final MenuStyle menuStyle = MenuStyle(
shape: MaterialStateProperty.all<OutlinedBorder?>(const RoundedRectangleBorder()),
elevation: MaterialStateProperty.all<double?>(20.0),
backgroundColor: const MaterialStatePropertyAll<Color>(Colors.green),
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
style: style,
menuStyle: menuStyle,
menuChildren: <Widget>[
MenuItemButton(
style: style,
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
final SubmenuButton submenu = tester.widget(find.byType(SubmenuButton));
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
submenu.debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(
description,
equalsIgnoringHashCodes(
<String>[
'child: Text("Menu 0")',
'focusNode: null',
'menuStyle: MenuStyle#00000(backgroundColor: MaterialStatePropertyAll(MaterialColor(primary value: Color(0xff4caf50))), elevation: MaterialStatePropertyAll(20.0), shape: MaterialStatePropertyAll(RoundedRectangleBorder(BorderSide(width: 0.0, style: none), BorderRadius.zero)))',
'alignmentOffset: null',
'clipBehavior: none',
],
),
);
});
});
group('Layout', () {
List<Rect> collectMenuRects() {
final List<Rect> menuRects = <Rect>[];
final List<Element> candidates = find.byType(SubmenuButton).evaluate().toList();
for (final Element candidate in candidates) {
final RenderBox box = candidate.renderObject! as RenderBox;
final Offset topLeft = box.localToGlobal(box.size.topLeft(Offset.zero));
final Offset bottomRight = box.localToGlobal(box.size.bottomRight(Offset.zero));
menuRects.add(Rect.fromPoints(topLeft, bottomRight));
}
return menuRects;
}
testWidgets('unconstrained menus show up in the right place in LTR', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(4));
final List<Rect> menuRects = collectMenuRects();
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 104.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(104.0, 0.0, 204.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(204.0, 0.0, 304.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(104.0, 100.0, 334.0, 148.0)));
});
testWidgets('unconstrained menus show up in the right place in RTL', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(800, 600));
await tester.pumpWidget(
MaterialApp(
home: Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: MenuBar(
children: createTestMenus(onPressed: onPressed),
),
),
],
),
const Expanded(child: Placeholder()),
],
),
),
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(4));
final List<Rect> menuRects = collectMenuRects();
expect(menuRects[0], equals(const Rect.fromLTRB(696.0, 0.0, 796.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(596.0, 0.0, 696.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(496.0, 0.0, 596.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(466.0, 100.0, 696.0, 148.0)));
});
testWidgets('constrained menus show up in the right place in LTR', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(300, 300));
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Column(
children: <Widget>[
MenuBar(
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(4));
final List<Rect> menuRects = collectMenuRects();
expect(menuRects[0], equals(const Rect.fromLTRB(4.0, 0.0, 104.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(104.0, 0.0, 204.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(204.0, 0.0, 304.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(70.0, 100.0, 300.0, 148.0)));
});
testWidgets('constrained menus show up in the right place in RTL', (WidgetTester tester) async {
await tester.binding.setSurfaceSize(const Size(300, 300));
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (BuildContext context) {
return Directionality(
textDirection: TextDirection.rtl,
child: Material(
child: Column(
children: <Widget>[
MenuBar(
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
);
await tester.pump();
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
await tester.tap(find.text(TestMenu.subMenu11.label));
await tester.pump();
expect(find.byType(MenuItemButton), findsNWidgets(6));
expect(find.byType(SubmenuButton), findsNWidgets(4));
final List<Rect> menuRects = collectMenuRects();
expect(menuRects[0], equals(const Rect.fromLTRB(196.0, 0.0, 296.0, 48.0)));
expect(menuRects[1], equals(const Rect.fromLTRB(96.0, 0.0, 196.0, 48.0)));
expect(menuRects[2], equals(const Rect.fromLTRB(-4.0, 0.0, 96.0, 48.0)));
expect(menuRects[3], equals(const Rect.fromLTRB(0.0, 100.0, 230.0, 148.0)));
});
});
group('LocalizedShortcutLabeler', () {
testWidgets('getShortcutLabel returns the right labels', (WidgetTester tester) async {
String expectedMeta;
String expectedCtrl;
String expectedAlt;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
expectedCtrl = 'Ctrl';
expectedMeta = 'Meta';
expectedAlt = 'Alt';
break;
case TargetPlatform.windows:
expectedCtrl = 'Ctrl';
expectedMeta = 'Win';
expectedAlt = 'Alt';
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
expectedCtrl = '⌃';
expectedMeta = '⌘';
expectedAlt = '⌥';
break;
}
const SingleActivator allModifiers = SingleActivator(
LogicalKeyboardKey.keyA,
control: true,
meta: true,
shift: true,
alt: true,
);
final String allExpected = '$expectedAlt $expectedCtrl $expectedMeta ⇧ A';
const CharacterActivator charShortcuts = CharacterActivator('ñ');
const String charExpected = 'ñ';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: MenuBar(
controller: controller,
children: <Widget>[
SubmenuButton(
menuChildren: <Widget>[
MenuItemButton(
shortcut: allModifiers,
child: Text(TestMenu.subMenu10.label),
),
MenuItemButton(
shortcut: charShortcuts,
child: Text(TestMenu.subMenu11.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
],
),
),
),
);
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
expect(find.text(allExpected), findsOneWidget);
expect(find.text(charExpected), findsOneWidget);
}, variant: TargetPlatformVariant.all());
});
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeStandard = false,
bool includeExtraGroups = false,
}) {
final List<Widget> result = <Widget>[
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
shortcut: shortcuts[TestMenu.subMenu00],
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(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
shortcut: shortcuts[TestMenu.subMenu10],
child: Text(TestMenu.subMenu10.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
menuChildren: <Widget>[
MenuItemButton(
key: UniqueKey(),
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
shortcut: shortcuts[TestMenu.subSubMenu110],
child: Text(TestMenu.subSubMenu110.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
shortcut: shortcuts[TestMenu.subSubMenu111],
child: Text(TestMenu.subSubMenu111.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
shortcut: shortcuts[TestMenu.subSubMenu112],
child: Text(TestMenu.subSubMenu112.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
shortcut: shortcuts[TestMenu.subSubMenu113],
child: Text(TestMenu.subSubMenu113.label),
),
],
child: Text(TestMenu.subMenu11.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
shortcut: shortcuts[TestMenu.subMenu12],
child: Text(TestMenu.subMenu12.label),
),
],
child: Text(TestMenu.mainMenu1.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu20],
child: Text(TestMenu.subMenu20.label),
),
],
child: Text(TestMenu.mainMenu2.label),
),
if (includeExtraGroups)
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu30],
// Always disabled.
child: Text(TestMenu.subMenu30.label),
),
],
child: Text(TestMenu.mainMenu3.label),
),
if (includeExtraGroups)
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu40],
// Always disabled.
child: Text(TestMenu.subMenu40.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu41],
// Always disabled.
child: Text(TestMenu.subMenu41.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu42],
// Always disabled.
child: Text(TestMenu.subMenu42.label),
),
],
child: Text(TestMenu.mainMenu4.label),
),
];
return result;
}
enum TestMenu {
mainMenu0('Menu 0'),
mainMenu1('Menu 1'),
mainMenu2('Menu 2'),
mainMenu3('Menu 3'),
mainMenu4('Menu 4'),
subMenu00('Sub Menu 00'),
subMenu01('Sub Menu 01'),
subMenu02('Sub Menu 02'),
subMenu10('Sub Menu 10'),
subMenu11('Sub Menu 11'),
subMenu12('Sub Menu 12'),
subMenu20('Sub Menu 20'),
subMenu30('Sub Menu 30'),
subMenu40('Sub Menu 40'),
subMenu41('Sub Menu 41'),
subMenu42('Sub Menu 42'),
subSubMenu110('Sub Sub Menu 110'),
subSubMenu111('Sub Sub Menu 111'),
subSubMenu112('Sub Sub Menu 112'),
subSubMenu113('Sub Sub Menu 113');
const TestMenu(this.label);
final String label;
}
// 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_test/flutter_test.dart';
void main() {
void onPressed(TestMenu item) {}
Finder findMenuPanels(Axis orientation) {
return find.byWidgetPredicate((Widget widget) {
// ignore: avoid_dynamic_calls
return widget.runtimeType.toString() == '_MenuPanel' && (widget as dynamic).orientation == orientation;
});
}
Finder findMenuBarPanel() {
return findMenuPanels(Axis.horizontal);
}
Finder findSubmenuPanel() {
return findMenuPanels(Axis.vertical);
}
Finder findSubMenuItem() {
return find.descendant(of: findSubmenuPanel().last, matching: find.byType(MenuItemButton));
}
Material getMenuBarPanelMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findMenuBarPanel(), matching: find.byType(Material)).first);
}
Material getSubmenuPanelMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findSubmenuPanel(), matching: find.byType(Material)).first);
}
DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) {
return tester.widget<DefaultTextStyle>(
find
.ancestor(
of: find.text(labelText),
matching: find.byType(DefaultTextStyle),
)
.first,
);
}
testWidgets('theme is honored', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Builder(builder: (BuildContext context) {
return MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
elevation: MaterialStatePropertyAll<double?>(20.0),
),
),
child: MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(15.0),
shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(10.0),
),
),
),
child: Column(
children: <Widget>[
MenuBar(
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
);
}),
),
),
);
// Open a test menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 68.0)));
final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
expect(menuBarMaterial.elevation, equals(15));
expect(menuBarMaterial.color, equals(Colors.red));
final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(350.0, 58.0, 580.0, 210.0)));
expect(subMenuMaterial.elevation, equals(20));
expect(subMenuMaterial.color, equals(Colors.green));
});
testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
elevation: MaterialStatePropertyAll<double?>(20.0),
),
),
child: MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(15.0),
shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(10.0),
),
),
),
child: Column(
children: <Widget>[
MenuBar(
style: const MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
elevation: MaterialStatePropertyAll<double?>(10.0),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(12.0),
),
),
children: createTestMenus(
onPressed: onPressed,
menuBackground: Colors.cyan,
menuElevation: 18.0,
menuPadding: const EdgeInsetsDirectional.all(14.0),
menuShape: const BeveledRectangleBorder(),
itemBackground: Colors.amber,
itemForeground: Colors.grey,
itemOverlay: Colors.blueGrey,
itemPadding: const EdgeInsetsDirectional.all(11.0),
itemShape: const StadiumBorder(),
),
),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
),
);
// Open a test menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(238.0, 0.0, 562.0, 72.0)));
final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
expect(menuBarMaterial.elevation, equals(10.0));
expect(menuBarMaterial.color, equals(Colors.blue));
final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(336.0, 60.0, 594.0, 232.0)));
expect(subMenuMaterial.elevation, equals(18));
expect(subMenuMaterial.color, equals(Colors.cyan));
expect(subMenuMaterial.shape, equals(const BeveledRectangleBorder()));
final Finder menuItem = findSubMenuItem();
expect(tester.getRect(menuItem.first), equals(const Rect.fromLTRB(350.0, 74.0, 580.0, 122.0)));
final Material menuItemMaterial = tester.widget<Material>(
find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).first);
expect(menuItemMaterial.color, equals(Colors.amber));
expect(menuItemMaterial.elevation, equals(0.0));
expect(menuItemMaterial.shape, equals(const StadiumBorder()));
expect(getLabelStyle(tester, TestMenu.subMenu10.label).style.color, equals(Colors.grey));
final ButtonStyle? textButtonStyle = tester
.widget<TextButton>(find
.ancestor(
of: find.text(TestMenu.subMenu10.label),
matching: find.byType(TextButton),
)
.first)
.style;
expect(textButtonStyle?.overlayColor?.resolve(<MaterialState>{MaterialState.hovered}), equals(Colors.blueGrey));
});
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeStandard = false,
Color? itemOverlay,
Color? itemBackground,
Color? itemForeground,
EdgeInsetsDirectional? itemPadding,
Color? menuBackground,
EdgeInsetsDirectional? menuPadding,
OutlinedBorder? menuShape,
double? menuElevation,
OutlinedBorder? itemShape,
}) {
final MenuStyle menuStyle = MenuStyle(
padding: menuPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding) : null,
backgroundColor: menuBackground != null ? MaterialStatePropertyAll<Color>(menuBackground) : null,
elevation: menuElevation != null ? MaterialStatePropertyAll<double>(menuElevation) : null,
shape: menuShape != null ? MaterialStatePropertyAll<OutlinedBorder>(menuShape) : null,
);
final ButtonStyle itemStyle = ButtonStyle(
padding: itemPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(itemPadding) : null,
shape: itemShape != null ? MaterialStatePropertyAll<OutlinedBorder>(itemShape) : null,
foregroundColor: itemForeground != null ? MaterialStatePropertyAll<Color>(itemForeground) : null,
backgroundColor: itemBackground != null ? MaterialStatePropertyAll<Color>(itemBackground) : null,
overlayColor: itemOverlay != null ? MaterialStatePropertyAll<Color>(itemOverlay) : null,
);
final List<Widget> result = <Widget>[
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
shortcut: shortcuts[TestMenu.subMenu00],
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
menuStyle: menuStyle,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
shortcut: shortcuts[TestMenu.subMenu10],
style: itemStyle,
child: Text(TestMenu.subMenu10.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
shortcut: shortcuts[TestMenu.subSubMenu110],
child: Text(TestMenu.subSubMenu110.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
shortcut: shortcuts[TestMenu.subSubMenu111],
child: Text(TestMenu.subSubMenu111.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
shortcut: shortcuts[TestMenu.subSubMenu112],
child: Text(TestMenu.subSubMenu112.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
shortcut: shortcuts[TestMenu.subSubMenu113],
child: Text(TestMenu.subSubMenu113.label),
),
],
child: Text(TestMenu.subMenu11.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
shortcut: shortcuts[TestMenu.subMenu12],
child: Text(TestMenu.subMenu12.label),
),
],
child: Text(TestMenu.mainMenu1.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu20],
// Always disabled.
child: Text(TestMenu.subMenu20.label),
),
],
child: Text(TestMenu.mainMenu2.label),
),
];
return result;
}
enum TestMenu {
mainMenu0('Menu 0'),
mainMenu1('Menu 1'),
mainMenu2('Menu 2'),
subMenu00('Sub Menu 00'),
subMenu10('Sub Menu 10'),
subMenu11('Sub Menu 11'),
subMenu12('Sub Menu 12'),
subMenu20('Sub Menu 20'),
subSubMenu110('Sub Sub Menu 110'),
subSubMenu111('Sub Sub Menu 111'),
subSubMenu112('Sub Sub Menu 112'),
subSubMenu113('Sub Sub Menu 113');
const TestMenu(this.label);
final String label;
}
// 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_test/flutter_test.dart';
void main() {
Finder findMenuPanels() {
return find.byWidgetPredicate((Widget widget) => widget.runtimeType.toString() == '_MenuPanel');
}
Material getMenuBarMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(of: findMenuPanels(), matching: find.byType(Material)).first,
);
}
Padding getMenuBarPadding(WidgetTester tester) {
return tester.widget<Padding>(
find.descendant(of: findMenuPanels(), matching: find.byType(Padding)).first,
);
}
Material getMenuMaterial(WidgetTester tester) {
return tester.widget<Material>(
find.descendant(of: findMenuPanels().at(1), matching: find.byType(Material)).first,
);
}
Padding getMenuPadding(WidgetTester tester) {
return tester.widget<Padding>(
find.descendant(of: findMenuPanels().at(1), matching: find.byType(Padding)).first,
);
}
group('MenuStyle', () {
testWidgets('fixedSize affects geometry', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
fixedSize: MaterialStatePropertyAll<Size>(Size(600, 60)),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
fixedSize: MaterialStatePropertyAll<Size>(Size(100, 100)),
),
),
child: MenuBar(
children: createTestMenus(onPressed: (TestMenu menu) {}),
),
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
// MenuBarTheme affects MenuBar.
expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(100.0, 0.0, 700.0, 60.0)));
expect(tester.getRect(findMenuPanels().first).size, equals(const Size(600.0, 60.0)));
// MenuTheme affects menus.
expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(104.0, 54.0, 204.0, 154.0)));
expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0)));
});
testWidgets('maximumSize affects geometry', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
maximumSize: MaterialStatePropertyAll<Size>(Size(250, 40)),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
maximumSize: MaterialStatePropertyAll<Size>(Size(100, 100)),
),
),
child: MenuBar(
children: createTestMenus(onPressed: (TestMenu menu) {}),
),
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
// MenuBarTheme affects MenuBar.
expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(275.0, 0.0, 525.0, 40.0)));
expect(tester.getRect(findMenuPanels().first).size, equals(const Size(250.0, 40.0)));
// MenuTheme affects menus.
expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(279.0, 44.0, 379.0, 144.0)));
expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(100.0, 100.0)));
});
testWidgets('minimumSize affects geometry', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
minimumSize: MaterialStatePropertyAll<Size>(Size(400, 60)),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
minimumSize: MaterialStatePropertyAll<Size>(Size(300, 300)),
),
),
child: MenuBar(
children: createTestMenus(onPressed: (TestMenu menu) {}),
),
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
// MenuBarTheme affects MenuBar.
expect(tester.getRect(findMenuPanels().first), equals(const Rect.fromLTRB(200.0, 0.0, 600.0, 60.0)));
expect(tester.getRect(findMenuPanels().first).size, equals(const Size(400.0, 60.0)));
// MenuTheme affects menus.
expect(tester.getRect(findMenuPanels().at(1)), equals(const Rect.fromLTRB(204.0, 54.0, 504.0, 354.0)));
expect(tester.getRect(findMenuPanels().at(1)).size, equals(const Size(300.0, 300.0)));
});
testWidgets('Material parameters are honored', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.red),
shadowColor: MaterialStatePropertyAll<Color>(Colors.green),
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.blue),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(10)),
elevation: MaterialStatePropertyAll<double>(10),
side: MaterialStatePropertyAll<BorderSide>(BorderSide(color: Colors.redAccent)),
shape: MaterialStatePropertyAll<OutlinedBorder>(StadiumBorder()),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color>(Colors.cyan),
shadowColor: MaterialStatePropertyAll<Color>(Colors.purple),
surfaceTintColor: MaterialStatePropertyAll<Color>(Colors.yellow),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(EdgeInsets.all(20)),
elevation: MaterialStatePropertyAll<double>(20),
side: MaterialStatePropertyAll<BorderSide>(BorderSide(color: Colors.cyanAccent)),
shape: MaterialStatePropertyAll<OutlinedBorder>(StarBorder()),
),
),
child: MenuBar(
children: createTestMenus(onPressed: (TestMenu menu) {}),
),
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
// Have to open a menu initially to start things going.
await tester.tap(find.text(TestMenu.mainMenu0.label));
await tester.pump();
final Material menuBarMaterial = getMenuBarMaterial(tester);
final Padding menuBarPadding = getMenuBarPadding(tester);
final Material panelMaterial = getMenuMaterial(tester);
final Padding panelPadding = getMenuPadding(tester);
// MenuBarTheme affects MenuBar.
expect(menuBarMaterial.color, equals(Colors.red));
expect(menuBarMaterial.shadowColor, equals(Colors.green));
expect(menuBarMaterial.surfaceTintColor, equals(Colors.blue));
expect(menuBarMaterial.shape, equals(const StadiumBorder(side: BorderSide(color: Colors.redAccent))));
expect(menuBarPadding.padding, equals(const EdgeInsets.all(10)));
// MenuBarTheme affects menus.
expect(panelMaterial.color, equals(Colors.cyan));
expect(panelMaterial.shadowColor, equals(Colors.purple));
expect(panelMaterial.surfaceTintColor, equals(Colors.yellow));
expect(panelMaterial.shape, equals(const StarBorder(side: BorderSide(color: Colors.cyanAccent))));
expect(panelPadding.padding, equals(const EdgeInsets.all(20)));
});
testWidgets('visual density', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Column(
children: <Widget>[
MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
visualDensity: VisualDensity(horizontal: 1.5, vertical: -1.5),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
visualDensity: VisualDensity(horizontal: 0.5, vertical: -0.5),
),
),
child: MenuBar(
children: createTestMenus(onPressed: (TestMenu menu) {}),
),
),
),
const Expanded(child: Placeholder()),
],
),
),
),
);
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 48.0)));
// Open and make sure things are the right size.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(find.byType(MenuBar)), equals(const Rect.fromLTRB(240.0, 0.0, 560.0, 48.0)));
expect(
tester.getRect(find.text(TestMenu.subMenu10.label)),
equals(const Rect.fromLTRB(366.0, 64.0, 520.0, 78.0)),
);
expect(
tester.getRect(find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).at(1)),
equals(const Rect.fromLTRB(350.0, 48.0, 602.0, 178.0)),
);
});
});
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeStandard = false,
bool includeExtraGroups = false,
}) {
final List<Widget> result = <Widget>[
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
shortcut: shortcuts[TestMenu.subMenu00],
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(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
shortcut: shortcuts[TestMenu.subMenu10],
child: Text(TestMenu.subMenu10.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
menuChildren: <Widget>[
MenuItemButton(
key: UniqueKey(),
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
shortcut: shortcuts[TestMenu.subSubMenu110],
child: Text(TestMenu.subSubMenu110.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
shortcut: shortcuts[TestMenu.subSubMenu111],
child: Text(TestMenu.subSubMenu111.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
shortcut: shortcuts[TestMenu.subSubMenu112],
child: Text(TestMenu.subSubMenu112.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
shortcut: shortcuts[TestMenu.subSubMenu113],
child: Text(TestMenu.subSubMenu113.label),
),
],
child: Text(TestMenu.subMenu11.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
shortcut: shortcuts[TestMenu.subMenu12],
child: Text(TestMenu.subMenu12.label),
),
],
child: Text(TestMenu.mainMenu1.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu20],
child: Text(TestMenu.subMenu20.label),
),
],
child: Text(TestMenu.mainMenu2.label),
),
if (includeExtraGroups)
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu3) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu3) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu30],
// Always disabled.
child: Text(TestMenu.subMenu30.label),
),
],
child: Text(TestMenu.mainMenu3.label),
),
if (includeExtraGroups)
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu4) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu4) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu40],
// Always disabled.
child: Text(TestMenu.subMenu40.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu41],
// Always disabled.
child: Text(TestMenu.subMenu41.label),
),
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu42],
// Always disabled.
child: Text(TestMenu.subMenu42.label),
),
],
child: Text(TestMenu.mainMenu4.label),
),
];
return result;
}
enum TestMenu {
mainMenu0('Menu 0'),
mainMenu1('Menu 1'),
mainMenu2('Menu 2'),
mainMenu3('Menu 3'),
mainMenu4('Menu 4'),
subMenu00('Sub Menu 00'),
subMenu01('Sub Menu 01'),
subMenu02('Sub Menu 02'),
subMenu10('Sub Menu 10'),
subMenu11('Sub Menu 11'),
subMenu12('Sub Menu 12'),
subMenu20('Sub Menu 20'),
subMenu30('Sub Menu 30'),
subMenu40('Sub Menu 40'),
subMenu41('Sub Menu 41'),
subMenu42('Sub Menu 42'),
subSubMenu110('Sub Sub Menu 110'),
subSubMenu111('Sub Sub Menu 111'),
subSubMenu112('Sub Sub Menu 112'),
subSubMenu113('Sub Sub Menu 113');
const TestMenu(this.label);
final String label;
}
// 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_test/flutter_test.dart';
void main() {
void onPressed(TestMenu item) {}
Finder findMenuPanels(Axis orientation) {
return find.byWidgetPredicate((Widget widget) {
// ignore: avoid_dynamic_calls
return widget.runtimeType.toString() == '_MenuPanel' && (widget as dynamic).orientation == orientation;
});
}
Finder findMenuBarPanel() {
return findMenuPanels(Axis.horizontal);
}
Finder findSubmenuPanel() {
return findMenuPanels(Axis.vertical);
}
Finder findSubMenuItem() {
return find.descendant(of: findSubmenuPanel().last, matching: find.byType(MenuItemButton));
}
Material getMenuBarPanelMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findMenuBarPanel(), matching: find.byType(Material)).first);
}
Material getSubmenuPanelMaterial(WidgetTester tester) {
return tester.widget<Material>(find.descendant(of: findSubmenuPanel(), matching: find.byType(Material)).first);
}
DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) {
return tester.widget<DefaultTextStyle>(
find
.ancestor(
of: find.text(labelText),
matching: find.byType(DefaultTextStyle),
)
.first,
);
}
testWidgets('theme is honored', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Builder(builder: (BuildContext context) {
return MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
elevation: MaterialStatePropertyAll<double?>(20.0),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(15.0),
shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(10.0),
),
),
),
child: Column(
children: <Widget>[
MenuBar(
children: createTestMenus(onPressed: onPressed),
),
const Expanded(child: Placeholder()),
],
),
),
);
}),
),
),
);
// Open a test menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(246.0, 0.0, 554.0, 48.0)));
final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
expect(menuBarMaterial.elevation, equals(20));
expect(menuBarMaterial.color, equals(Colors.green));
final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(340.0, 48.0, 590.0, 212.0)));
expect(subMenuMaterial.elevation, equals(15));
expect(subMenuMaterial.color, equals(Colors.red));
});
testWidgets('Constructor parameters override theme parameters', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Builder(
builder: (BuildContext context) {
return MenuBarTheme(
data: const MenuBarThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.green),
elevation: MaterialStatePropertyAll<double?>(20.0),
),
),
child: MenuTheme(
data: const MenuThemeData(
style: MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.red),
elevation: MaterialStatePropertyAll<double?>(15.0),
shape: MaterialStatePropertyAll<OutlinedBorder?>(StadiumBorder()),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(10.0),
),
),
),
child: Column(
children: <Widget>[
MenuBar(
style: const MenuStyle(
backgroundColor: MaterialStatePropertyAll<Color?>(Colors.blue),
elevation: MaterialStatePropertyAll<double?>(10.0),
padding: MaterialStatePropertyAll<EdgeInsetsGeometry>(
EdgeInsetsDirectional.all(12.0),
),
),
children: createTestMenus(
onPressed: onPressed,
menuBackground: Colors.cyan,
menuElevation: 18.0,
menuPadding: const EdgeInsetsDirectional.all(14.0),
menuShape: const BeveledRectangleBorder(),
itemBackground: Colors.amber,
itemForeground: Colors.grey,
itemOverlay: Colors.blueGrey,
itemPadding: const EdgeInsetsDirectional.all(11.0),
itemShape: const StadiumBorder(),
),
),
const Expanded(child: Placeholder()),
],
),
),
);
},
),
),
),
);
// Open a test menu.
await tester.tap(find.text(TestMenu.mainMenu1.label));
await tester.pump();
expect(tester.getRect(findMenuBarPanel().first), equals(const Rect.fromLTRB(238.0, 0.0, 562.0, 72.0)));
final Material menuBarMaterial = getMenuBarPanelMaterial(tester);
expect(menuBarMaterial.elevation, equals(10.0));
expect(menuBarMaterial.color, equals(Colors.blue));
final Material subMenuMaterial = getSubmenuPanelMaterial(tester);
expect(tester.getRect(findSubmenuPanel()), equals(const Rect.fromLTRB(336.0, 60.0, 594.0, 232.0)));
expect(subMenuMaterial.elevation, equals(18));
expect(subMenuMaterial.color, equals(Colors.cyan));
expect(subMenuMaterial.shape, equals(const BeveledRectangleBorder()));
final Finder menuItem = findSubMenuItem();
expect(tester.getRect(menuItem.first), equals(const Rect.fromLTRB(350.0, 74.0, 580.0, 122.0)));
final Material menuItemMaterial = tester.widget<Material>(
find.ancestor(of: find.text(TestMenu.subMenu10.label), matching: find.byType(Material)).first);
expect(menuItemMaterial.color, equals(Colors.amber));
expect(menuItemMaterial.elevation, equals(0.0));
expect(menuItemMaterial.shape, equals(const StadiumBorder()));
expect(getLabelStyle(tester, TestMenu.subMenu10.label).style.color, equals(Colors.grey));
final ButtonStyle? textButtonStyle = tester
.widget<TextButton>(find
.ancestor(
of: find.text(TestMenu.subMenu10.label),
matching: find.byType(TextButton),
)
.first)
.style;
expect(textButtonStyle?.overlayColor?.resolve(<MaterialState>{MaterialState.hovered}), equals(Colors.blueGrey));
});
}
List<Widget> createTestMenus({
void Function(TestMenu)? onPressed,
void Function(TestMenu)? onOpen,
void Function(TestMenu)? onClose,
Map<TestMenu, MenuSerializableShortcut> shortcuts = const <TestMenu, MenuSerializableShortcut>{},
bool includeStandard = false,
Color? itemOverlay,
Color? itemBackground,
Color? itemForeground,
EdgeInsetsDirectional? itemPadding,
Color? menuBackground,
EdgeInsetsDirectional? menuPadding,
OutlinedBorder? menuShape,
double? menuElevation,
OutlinedBorder? itemShape,
}) {
final MenuStyle menuStyle = MenuStyle(
padding: menuPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(menuPadding) : null,
backgroundColor: menuBackground != null ? MaterialStatePropertyAll<Color>(menuBackground) : null,
elevation: menuElevation != null ? MaterialStatePropertyAll<double>(menuElevation) : null,
shape: menuShape != null ? MaterialStatePropertyAll<OutlinedBorder>(menuShape) : null,
);
final ButtonStyle itemStyle = ButtonStyle(
padding: itemPadding != null ? MaterialStatePropertyAll<EdgeInsetsGeometry>(itemPadding) : null,
shape: itemShape != null ? MaterialStatePropertyAll<OutlinedBorder>(itemShape) : null,
foregroundColor: itemForeground != null ? MaterialStatePropertyAll<Color>(itemForeground) : null,
backgroundColor: itemBackground != null ? MaterialStatePropertyAll<Color>(itemBackground) : null,
overlayColor: itemOverlay != null ? MaterialStatePropertyAll<Color>(itemOverlay) : null,
);
final List<Widget> result = <Widget>[
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu0) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu0) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu00) : null,
shortcut: shortcuts[TestMenu.subMenu00],
child: Text(TestMenu.subMenu00.label),
),
],
child: Text(TestMenu.mainMenu0.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu1) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu1) : null,
menuStyle: menuStyle,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu10) : null,
shortcut: shortcuts[TestMenu.subMenu10],
style: itemStyle,
child: Text(TestMenu.subMenu10.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.subMenu11) : null,
onClose: onClose != null ? () => onClose(TestMenu.subMenu11) : null,
menuChildren: <Widget>[
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu110) : null,
shortcut: shortcuts[TestMenu.subSubMenu110],
child: Text(TestMenu.subSubMenu110.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu111) : null,
shortcut: shortcuts[TestMenu.subSubMenu111],
child: Text(TestMenu.subSubMenu111.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu112) : null,
shortcut: shortcuts[TestMenu.subSubMenu112],
child: Text(TestMenu.subSubMenu112.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subSubMenu113) : null,
shortcut: shortcuts[TestMenu.subSubMenu113],
child: Text(TestMenu.subSubMenu113.label),
),
],
child: Text(TestMenu.subMenu11.label),
),
MenuItemButton(
onPressed: onPressed != null ? () => onPressed(TestMenu.subMenu12) : null,
shortcut: shortcuts[TestMenu.subMenu12],
child: Text(TestMenu.subMenu12.label),
),
],
child: Text(TestMenu.mainMenu1.label),
),
SubmenuButton(
onOpen: onOpen != null ? () => onOpen(TestMenu.mainMenu2) : null,
onClose: onClose != null ? () => onClose(TestMenu.mainMenu2) : null,
menuChildren: <Widget>[
MenuItemButton(
// Always disabled.
shortcut: shortcuts[TestMenu.subMenu20],
// Always disabled.
child: Text(TestMenu.subMenu20.label),
),
],
child: Text(TestMenu.mainMenu2.label),
),
];
return result;
}
enum TestMenu {
mainMenu0('Menu 0'),
mainMenu1('Menu 1'),
mainMenu2('Menu 2'),
subMenu00('Sub Menu 00'),
subMenu10('Sub Menu 10'),
subMenu11('Sub Menu 11'),
subMenu12('Sub Menu 12'),
subMenu20('Sub Menu 20'),
subSubMenu110('Sub Sub Menu 110'),
subSubMenu111('Sub Sub Menu 111'),
subSubMenu112('Sub Sub Menu 112'),
subSubMenu113('Sub Sub Menu 113');
const TestMenu(this.label);
final String label;
}
...@@ -6,62 +6,6 @@ import 'package:flutter/foundation.dart'; ...@@ -6,62 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@immutable
class MyThemeExtensionA extends ThemeExtension<MyThemeExtensionA> {
const MyThemeExtensionA({
required this.color1,
required this.color2,
});
final Color? color1;
final Color? color2;
@override
MyThemeExtensionA copyWith({Color? color1, Color? color2}) {
return MyThemeExtensionA(
color1: color1 ?? this.color1,
color2: color2 ?? this.color2,
);
}
@override
MyThemeExtensionA lerp(MyThemeExtensionA? other, double t) {
if (other is! MyThemeExtensionA) {
return this;
}
return MyThemeExtensionA(
color1: Color.lerp(color1, other.color1, t),
color2: Color.lerp(color2, other.color2, t),
);
}
}
@immutable
class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
const MyThemeExtensionB({
required this.textStyle,
});
final TextStyle? textStyle;
@override
MyThemeExtensionB copyWith({Color? color, TextStyle? textStyle}) {
return MyThemeExtensionB(
textStyle: textStyle ?? this.textStyle,
);
}
@override
MyThemeExtensionB lerp(MyThemeExtensionB? other, double t) {
if (other is! MyThemeExtensionB) {
return this;
}
return MyThemeExtensionB(
textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
);
}
}
void main() { void main() {
test('Theme data control test', () { test('Theme data control test', () {
final ThemeData dark = ThemeData.dark(); final ThemeData dark = ThemeData.dark();
...@@ -696,6 +640,9 @@ void main() { ...@@ -696,6 +640,9 @@ void main() {
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)), iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)),
listTileTheme: const ListTileThemeData(), listTileTheme: const ListTileThemeData(),
menuBarTheme: const MenuBarThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.black))),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black), navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.black),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black), navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)), outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
...@@ -810,6 +757,9 @@ void main() { ...@@ -810,6 +757,9 @@ void main() {
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white), floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
iconButtonTheme: const IconButtonThemeData(), iconButtonTheme: const IconButtonThemeData(),
listTileTheme: const ListTileThemeData(), listTileTheme: const ListTileThemeData(),
menuBarTheme: const MenuBarThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
menuButtonTheme: MenuButtonThemeData(style: MenuItemButton.styleFrom(backgroundColor: Colors.black)),
menuTheme: const MenuThemeData(style: MenuStyle(backgroundColor: MaterialStatePropertyAll<Color>(Colors.white))),
navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white), navigationBarTheme: const NavigationBarThemeData(backgroundColor: Colors.white),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white), navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
outlinedButtonTheme: const OutlinedButtonThemeData(), outlinedButtonTheme: const OutlinedButtonThemeData(),
...@@ -910,6 +860,9 @@ void main() { ...@@ -910,6 +860,9 @@ void main() {
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme, floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
iconButtonTheme: otherTheme.iconButtonTheme, iconButtonTheme: otherTheme.iconButtonTheme,
listTileTheme: otherTheme.listTileTheme, listTileTheme: otherTheme.listTileTheme,
menuBarTheme: otherTheme.menuBarTheme,
menuButtonTheme: otherTheme.menuButtonTheme,
menuTheme: otherTheme.menuTheme,
navigationBarTheme: otherTheme.navigationBarTheme, navigationBarTheme: otherTheme.navigationBarTheme,
navigationRailTheme: otherTheme.navigationRailTheme, navigationRailTheme: otherTheme.navigationRailTheme,
outlinedButtonTheme: otherTheme.outlinedButtonTheme, outlinedButtonTheme: otherTheme.outlinedButtonTheme,
...@@ -1009,6 +962,9 @@ void main() { ...@@ -1009,6 +962,9 @@ void main() {
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme)); expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme)); expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme));
expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme)); expect(themeDataCopy.listTileTheme, equals(otherTheme.listTileTheme));
expect(themeDataCopy.menuBarTheme, equals(otherTheme.menuBarTheme));
expect(themeDataCopy.menuButtonTheme, equals(otherTheme.menuButtonTheme));
expect(themeDataCopy.menuTheme, equals(otherTheme.menuTheme));
expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme)); expect(themeDataCopy.navigationBarTheme, equals(otherTheme.navigationBarTheme));
expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme)); expect(themeDataCopy.navigationRailTheme, equals(otherTheme.navigationRailTheme));
expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme)); expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme));
...@@ -1023,8 +979,6 @@ void main() { ...@@ -1023,8 +979,6 @@ void main() {
expect(themeDataCopy.textSelectionTheme, equals(otherTheme.textSelectionTheme)); expect(themeDataCopy.textSelectionTheme, equals(otherTheme.textSelectionTheme));
expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor)); expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor));
expect(themeDataCopy.textSelectionTheme.cursorColor, equals(otherTheme.textSelectionTheme.cursorColor)); expect(themeDataCopy.textSelectionTheme.cursorColor, equals(otherTheme.textSelectionTheme.cursorColor));
expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor));
expect(themeDataCopy.textSelectionTheme.cursorColor, equals(otherTheme.textSelectionTheme.cursorColor));
expect(themeDataCopy.textSelectionTheme.selectionHandleColor, equals(otherTheme.textSelectionTheme.selectionHandleColor)); expect(themeDataCopy.textSelectionTheme.selectionHandleColor, equals(otherTheme.textSelectionTheme.selectionHandleColor));
expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme)); expect(themeDataCopy.timePickerTheme, equals(otherTheme.timePickerTheme));
expect(themeDataCopy.toggleButtonsTheme, equals(otherTheme.toggleButtonsTheme)); expect(themeDataCopy.toggleButtonsTheme, equals(otherTheme.toggleButtonsTheme));
...@@ -1141,10 +1095,14 @@ void main() { ...@@ -1141,10 +1095,14 @@ void main() {
'dividerTheme', 'dividerTheme',
'drawerTheme', 'drawerTheme',
'elevatedButtonTheme', 'elevatedButtonTheme',
'expansionTileTheme',
'filledButtonTheme', 'filledButtonTheme',
'floatingActionButtonTheme', 'floatingActionButtonTheme',
'iconButtonTheme', 'iconButtonTheme',
'listTileTheme', 'listTileTheme',
'menuBarTheme',
'menuButtonTheme',
'menuTheme',
'navigationBarTheme', 'navigationBarTheme',
'navigationRailTheme', 'navigationRailTheme',
'outlinedButtonTheme', 'outlinedButtonTheme',
...@@ -1160,7 +1118,6 @@ void main() { ...@@ -1160,7 +1118,6 @@ void main() {
'timePickerTheme', 'timePickerTheme',
'toggleButtonsTheme', 'toggleButtonsTheme',
'tooltipTheme', 'tooltipTheme',
'expansionTileTheme',
// DEPRECATED (newest deprecations at the bottom) // DEPRECATED (newest deprecations at the bottom)
'accentColor', 'accentColor',
'accentColorBrightness', 'accentColorBrightness',
...@@ -1192,3 +1149,59 @@ void main() { ...@@ -1192,3 +1149,59 @@ void main() {
expect(propertyNames, expectedPropertyNames); expect(propertyNames, expectedPropertyNames);
}); });
} }
@immutable
class MyThemeExtensionA extends ThemeExtension<MyThemeExtensionA> {
const MyThemeExtensionA({
required this.color1,
required this.color2,
});
final Color? color1;
final Color? color2;
@override
MyThemeExtensionA copyWith({Color? color1, Color? color2}) {
return MyThemeExtensionA(
color1: color1 ?? this.color1,
color2: color2 ?? this.color2,
);
}
@override
MyThemeExtensionA lerp(MyThemeExtensionA? other, double t) {
if (other is! MyThemeExtensionA) {
return this;
}
return MyThemeExtensionA(
color1: Color.lerp(color1, other.color1, t),
color2: Color.lerp(color2, other.color2, t),
);
}
}
@immutable
class MyThemeExtensionB extends ThemeExtension<MyThemeExtensionB> {
const MyThemeExtensionB({
required this.textStyle,
});
final TextStyle? textStyle;
@override
MyThemeExtensionB copyWith({Color? color, TextStyle? textStyle}) {
return MyThemeExtensionB(
textStyle: textStyle ?? this.textStyle,
);
}
@override
MyThemeExtensionB lerp(MyThemeExtensionB? other, double t) {
if (other is! MyThemeExtensionB) {
return this;
}
return MyThemeExtensionB(
textStyle: TextStyle.lerp(textStyle, other.textStyle, t),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment