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
This diff is collapsed.
// 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';
export 'src/material/material_localizations.dart';
export 'src/material/material_state.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/navigation_bar.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;
}
This diff is collapsed.
// 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 {
/// 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 [ButtonStyle]'s foreground color. The button's [InkWell] adds
/// the style's overlay color when the button is focused, hovered
/// or pressed. The button's background color becomes its [Material]
/// 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
/// `Theme.of(context).foo`. Color scheme values like
......@@ -245,6 +246,7 @@ class TextButton extends ButtonStyleButton {
///
/// The color of the [ButtonStyle.textStyle] is not used, the
/// [ButtonStyle.foregroundColor] color is used instead.
/// {@endtemplate}
///
/// ## Material 2 defaults
///
......@@ -295,6 +297,7 @@ class TextButton extends ButtonStyleButton {
/// If [ThemeData.useMaterial3] is set to true the following defaults will
/// be used:
///
/// {@template flutter.material.text_button.material3_defaults}
/// * `textStyle` - Theme.textTheme.labelLarge
/// * `backgroundColor` - transparent
/// * `foregroundColor`
......@@ -326,6 +329,7 @@ class TextButton extends ButtonStyleButton {
/// * `enableFeedback` - true
/// * `alignment` - Alignment.center
/// * `splashFactory` - Theme.splashFactory
/// {@endtemplate}
@override
ButtonStyle defaultStyleOf(BuildContext context) {
final ThemeData theme = Theme.of(context);
......
......@@ -36,6 +36,9 @@ import 'ink_well.dart' show InteractiveInkFeatureFactory;
import 'input_decorator.dart';
import 'list_tile.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_rail_theme.dart';
import 'outlined_button_theme.dart';
......@@ -345,6 +348,9 @@ class ThemeData with Diagnosticable {
FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme,
MenuBarThemeData? menuBarTheme,
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme,
......@@ -568,17 +574,21 @@ class ThemeData with Diagnosticable {
bottomSheetTheme ??= const BottomSheetThemeData();
buttonBarTheme ??= const ButtonBarThemeData();
cardTheme ??= const CardTheme();
chipTheme ??= const ChipThemeData();
checkboxTheme ??= const CheckboxThemeData();
chipTheme ??= const ChipThemeData();
dataTableTheme ??= const DataTableThemeData();
dialogTheme ??= const DialogTheme();
dividerTheme ??= const DividerThemeData();
drawerTheme ??= const DrawerThemeData();
elevatedButtonTheme ??= const ElevatedButtonThemeData();
expansionTileTheme ??= const ExpansionTileThemeData();
filledButtonTheme ??= const FilledButtonThemeData();
floatingActionButtonTheme ??= const FloatingActionButtonThemeData();
iconButtonTheme ??= const IconButtonThemeData();
listTileTheme ??= const ListTileThemeData();
menuBarTheme ??= const MenuBarThemeData();
menuButtonTheme ??= const MenuButtonThemeData();
menuTheme ??= const MenuThemeData();
navigationBarTheme ??= const NavigationBarThemeData();
navigationRailTheme ??= const NavigationRailThemeData();
outlinedButtonTheme ??= const OutlinedButtonThemeData();
......@@ -594,7 +604,6 @@ class ThemeData with Diagnosticable {
timePickerTheme ??= const TimePickerThemeData();
toggleButtonsTheme ??= const ToggleButtonsThemeData();
tooltipTheme ??= const TooltipThemeData();
expansionTileTheme ??= const ExpansionTileThemeData();
// DEPRECATED (newest deprecations at the bottom)
accentTextTheme = defaultAccentTextTheme.merge(accentTextTheme);
......@@ -671,6 +680,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme,
listTileTheme: listTileTheme,
menuBarTheme: menuBarTheme,
menuButtonTheme: menuButtonTheme,
menuTheme: menuTheme,
navigationBarTheme: navigationBarTheme,
navigationRailTheme: navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme,
......@@ -778,6 +790,9 @@ class ThemeData with Diagnosticable {
required this.floatingActionButtonTheme,
required this.iconButtonTheme,
required this.listTileTheme,
required this.menuBarTheme,
required this.menuButtonTheme,
required this.menuTheme,
required this.navigationBarTheme,
required this.navigationRailTheme,
required this.outlinedButtonTheme,
......@@ -943,6 +958,9 @@ class ThemeData with Diagnosticable {
assert(floatingActionButtonTheme != null),
assert(iconButtonTheme != null),
assert(listTileTheme != null),
assert(menuBarTheme != null),
assert(menuButtonTheme != null),
assert(menuTheme != null),
assert(navigationBarTheme != null),
assert(navigationRailTheme != null),
assert(outlinedButtonTheme != null),
......@@ -1520,6 +1538,18 @@ class ThemeData with Diagnosticable {
/// A theme for customizing the appearance of [ListTile] widgets.
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
/// of a [NavigationBar].
final NavigationBarThemeData navigationBarTheme;
......@@ -1814,6 +1844,9 @@ class ThemeData with Diagnosticable {
FloatingActionButtonThemeData? floatingActionButtonTheme,
IconButtonThemeData? iconButtonTheme,
ListTileThemeData? listTileTheme,
MenuBarThemeData? menuBarTheme,
MenuButtonThemeData? menuButtonTheme,
MenuThemeData? menuTheme,
NavigationBarThemeData? navigationBarTheme,
NavigationRailThemeData? navigationRailTheme,
OutlinedButtonThemeData? outlinedButtonTheme,
......@@ -1972,6 +2005,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: floatingActionButtonTheme ?? this.floatingActionButtonTheme,
iconButtonTheme: iconButtonTheme ?? this.iconButtonTheme,
listTileTheme: listTileTheme ?? this.listTileTheme,
menuBarTheme: menuBarTheme ?? this.menuBarTheme,
menuButtonTheme: menuButtonTheme ?? this.menuButtonTheme,
menuTheme: menuTheme ?? this.menuTheme,
navigationBarTheme: navigationBarTheme ?? this.navigationBarTheme,
navigationRailTheme: navigationRailTheme ?? this.navigationRailTheme,
outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme,
......@@ -2172,6 +2208,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme: FloatingActionButtonThemeData.lerp(a.floatingActionButtonTheme, b.floatingActionButtonTheme, t)!,
iconButtonTheme: IconButtonThemeData.lerp(a.iconButtonTheme, b.iconButtonTheme, 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)!,
navigationRailTheme: NavigationRailThemeData.lerp(a.navigationRailTheme, b.navigationRailTheme, t)!,
outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t)!,
......@@ -2274,6 +2313,9 @@ class ThemeData with Diagnosticable {
other.floatingActionButtonTheme == floatingActionButtonTheme &&
other.iconButtonTheme == iconButtonTheme &&
other.listTileTheme == listTileTheme &&
other.menuBarTheme == menuBarTheme &&
other.menuButtonTheme == menuButtonTheme &&
other.menuTheme == menuTheme &&
other.navigationBarTheme == navigationBarTheme &&
other.navigationRailTheme == navigationRailTheme &&
other.outlinedButtonTheme == outlinedButtonTheme &&
......@@ -2373,6 +2415,9 @@ class ThemeData with Diagnosticable {
floatingActionButtonTheme,
iconButtonTheme,
listTileTheme,
menuBarTheme,
menuButtonTheme,
menuTheme,
navigationBarTheme,
navigationRailTheme,
outlinedButtonTheme,
......@@ -2474,6 +2519,9 @@ class ThemeData with Diagnosticable {
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<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<NavigationRailThemeData>('navigationRailTheme', navigationRailTheme, defaultValue: defaultData.navigationRailTheme, level: DiagnosticLevel.debug));
properties.add(DiagnosticsProperty<OutlinedButtonThemeData>('outlinedButtonTheme', outlinedButtonTheme, defaultValue: defaultData.outlinedButtonTheme, level: DiagnosticLevel.debug));
......@@ -2839,7 +2887,7 @@ class VisualDensity with Diagnosticable {
Offset get baseSizeAdjustment {
// The number of logical pixels represented by an increase or decrease in
// 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;
return Offset(horizontal, vertical) * interval;
......
......@@ -2419,7 +2419,9 @@ class LayerLink {
Size? leaderSize;
@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].
......
......@@ -4996,7 +4996,7 @@ class RenderFollowerLayer extends RenderProxyBox {
void paint(PaintingContext context, Offset offset) {
final Size? leaderSize = link.leaderSize;
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. '
'leaderSize is required when leaderAnchor is not Alignment.topLeft '
'(current value is $leaderAnchor).',
......
......@@ -85,7 +85,7 @@ export 'package:flutter/services.dart' show
/// infrequently change. This provides a performance tradeoff where building
/// the [Widget]s is faster but performing updates is slower.
///
/// | | _UbiquitiousInheritedElement | InheritedElement |
/// | | _UbiquitousInheritedElement | InheritedElement |
/// |---------------------|------------------------------|------------------|
/// | insert (best case) | O(1) | O(1) |
/// | insert (worst case) | O(1) | O(n) |
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -6,62 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.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() {
test('Theme data control test', () {
final ThemeData dark = ThemeData.dark();
......@@ -696,6 +640,9 @@ void main() {
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.black),
iconButtonTheme: IconButtonThemeData(style: IconButton.styleFrom(foregroundColor: Colors.pink)),
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),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.black),
outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(foregroundColor: Colors.blue)),
......@@ -810,6 +757,9 @@ void main() {
floatingActionButtonTheme: const FloatingActionButtonThemeData(backgroundColor: Colors.white),
iconButtonTheme: const IconButtonThemeData(),
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),
navigationRailTheme: const NavigationRailThemeData(backgroundColor: Colors.white),
outlinedButtonTheme: const OutlinedButtonThemeData(),
......@@ -910,6 +860,9 @@ void main() {
floatingActionButtonTheme: otherTheme.floatingActionButtonTheme,
iconButtonTheme: otherTheme.iconButtonTheme,
listTileTheme: otherTheme.listTileTheme,
menuBarTheme: otherTheme.menuBarTheme,
menuButtonTheme: otherTheme.menuButtonTheme,
menuTheme: otherTheme.menuTheme,
navigationBarTheme: otherTheme.navigationBarTheme,
navigationRailTheme: otherTheme.navigationRailTheme,
outlinedButtonTheme: otherTheme.outlinedButtonTheme,
......@@ -1009,6 +962,9 @@ void main() {
expect(themeDataCopy.floatingActionButtonTheme, equals(otherTheme.floatingActionButtonTheme));
expect(themeDataCopy.iconButtonTheme, equals(otherTheme.iconButtonTheme));
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.navigationRailTheme, equals(otherTheme.navigationRailTheme));
expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme));
......@@ -1023,8 +979,6 @@ void main() {
expect(themeDataCopy.textSelectionTheme, equals(otherTheme.textSelectionTheme));
expect(themeDataCopy.textSelectionTheme.selectionColor, equals(otherTheme.textSelectionTheme.selectionColor));
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.timePickerTheme, equals(otherTheme.timePickerTheme));
expect(themeDataCopy.toggleButtonsTheme, equals(otherTheme.toggleButtonsTheme));
......@@ -1141,10 +1095,14 @@ void main() {
'dividerTheme',
'drawerTheme',
'elevatedButtonTheme',
'expansionTileTheme',
'filledButtonTheme',
'floatingActionButtonTheme',
'iconButtonTheme',
'listTileTheme',
'menuBarTheme',
'menuButtonTheme',
'menuTheme',
'navigationBarTheme',
'navigationRailTheme',
'outlinedButtonTheme',
......@@ -1160,7 +1118,6 @@ void main() {
'timePickerTheme',
'toggleButtonsTheme',
'tooltipTheme',
'expansionTileTheme',
// DEPRECATED (newest deprecations at the bottom)
'accentColor',
'accentColorBrightness',
......@@ -1192,3 +1149,59 @@ void main() {
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