1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/material.dart'; |
7 | import 'package:flutter/services.dart'; |
8 | |
9 | /// Flutter code sample for [MenuAnchor]. |
10 | |
11 | void main() => runApp(const ContextMenuApp()); |
12 | |
13 | /// An enhanced enum to define the available menus and their shortcuts. |
14 | /// |
15 | /// Using an enum for menu definition is not required, but this illustrates how |
16 | /// they could be used for simple menu systems. |
17 | enum MenuEntry { |
18 | about('About' ), |
19 | showMessage('Show Message' , SingleActivator(LogicalKeyboardKey.keyS, control: true)), |
20 | hideMessage('Hide Message' , SingleActivator(LogicalKeyboardKey.keyS, control: true)), |
21 | colorMenu('Color Menu' ), |
22 | colorRed('Red Background' , SingleActivator(LogicalKeyboardKey.keyR, control: true)), |
23 | colorGreen('Green Background' , SingleActivator(LogicalKeyboardKey.keyG, control: true)), |
24 | colorBlue('Blue Background' , SingleActivator(LogicalKeyboardKey.keyB, control: true)); |
25 | |
26 | const MenuEntry(this.label, [this.shortcut]); |
27 | final String label; |
28 | final MenuSerializableShortcut? shortcut; |
29 | } |
30 | |
31 | class MyContextMenu extends StatefulWidget { |
32 | const MyContextMenu({super.key, required this.message}); |
33 | |
34 | final String message; |
35 | |
36 | @override |
37 | State<MyContextMenu> createState() => _MyContextMenuState(); |
38 | } |
39 | |
40 | class _MyContextMenuState extends State<MyContextMenu> { |
41 | MenuEntry? _lastSelection; |
42 | final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button' ); |
43 | final MenuController _menuController = MenuController(); |
44 | ShortcutRegistryEntry? _shortcutsEntry; |
45 | bool _menuWasEnabled = false; |
46 | |
47 | Color get backgroundColor => _backgroundColor; |
48 | Color _backgroundColor = Colors.red; |
49 | set backgroundColor(Color value) { |
50 | if (_backgroundColor != value) { |
51 | setState(() { |
52 | _backgroundColor = value; |
53 | }); |
54 | } |
55 | } |
56 | |
57 | bool get showingMessage => _showingMessage; |
58 | bool _showingMessage = false; |
59 | set showingMessage(bool value) { |
60 | if (_showingMessage != value) { |
61 | setState(() { |
62 | _showingMessage = value; |
63 | }); |
64 | } |
65 | } |
66 | |
67 | @override |
68 | void initState() { |
69 | super.initState(); |
70 | _disableContextMenu(); |
71 | } |
72 | |
73 | @override |
74 | void didChangeDependencies() { |
75 | super.didChangeDependencies(); |
76 | // Dispose of any previously registered shortcuts, since they are about to |
77 | // be replaced. |
78 | _shortcutsEntry?.dispose(); |
79 | // Collect the shortcuts from the different menu selections so that they can |
80 | // be registered to apply to the entire app. Menus don't register their |
81 | // shortcuts, they only display the shortcut hint text. |
82 | final Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{ |
83 | for (final MenuEntry item in MenuEntry.values) |
84 | if (item.shortcut != null) item.shortcut!: VoidCallbackIntent(() => _activate(item)), |
85 | }; |
86 | // Register the shortcuts with the ShortcutRegistry so that they are |
87 | // available to the entire application. |
88 | _shortcutsEntry = ShortcutRegistry.of(context).addAll(shortcuts); |
89 | } |
90 | |
91 | @override |
92 | void dispose() { |
93 | _shortcutsEntry?.dispose(); |
94 | _buttonFocusNode.dispose(); |
95 | _reenableContextMenu(); |
96 | super.dispose(); |
97 | } |
98 | |
99 | Future<void> _disableContextMenu() async { |
100 | if (!kIsWeb) { |
101 | // Does nothing on non-web platforms. |
102 | return; |
103 | } |
104 | _menuWasEnabled = BrowserContextMenu.enabled; |
105 | if (_menuWasEnabled) { |
106 | await BrowserContextMenu.disableContextMenu(); |
107 | } |
108 | } |
109 | |
110 | void _reenableContextMenu() { |
111 | if (!kIsWeb) { |
112 | // Does nothing on non-web platforms. |
113 | return; |
114 | } |
115 | if (_menuWasEnabled && !BrowserContextMenu.enabled) { |
116 | BrowserContextMenu.enableContextMenu(); |
117 | } |
118 | } |
119 | |
120 | @override |
121 | Widget build(BuildContext context) { |
122 | return Padding( |
123 | padding: const EdgeInsets.all(50), |
124 | child: GestureDetector( |
125 | onTapDown: _handleTapDown, |
126 | onSecondaryTapDown: _handleSecondaryTapDown, |
127 | child: MenuAnchor( |
128 | controller: _menuController, |
129 | menuChildren: <Widget>[ |
130 | MenuItemButton( |
131 | child: Text(MenuEntry.about.label), |
132 | onPressed: () => _activate(MenuEntry.about), |
133 | ), |
134 | if (_showingMessage) |
135 | MenuItemButton( |
136 | onPressed: () => _activate(MenuEntry.hideMessage), |
137 | shortcut: MenuEntry.hideMessage.shortcut, |
138 | child: Text(MenuEntry.hideMessage.label), |
139 | ), |
140 | if (!_showingMessage) |
141 | MenuItemButton( |
142 | onPressed: () => _activate(MenuEntry.showMessage), |
143 | shortcut: MenuEntry.showMessage.shortcut, |
144 | child: Text(MenuEntry.showMessage.label), |
145 | ), |
146 | SubmenuButton( |
147 | menuChildren: <Widget>[ |
148 | MenuItemButton( |
149 | onPressed: () => _activate(MenuEntry.colorRed), |
150 | shortcut: MenuEntry.colorRed.shortcut, |
151 | child: Text(MenuEntry.colorRed.label), |
152 | ), |
153 | MenuItemButton( |
154 | onPressed: () => _activate(MenuEntry.colorGreen), |
155 | shortcut: MenuEntry.colorGreen.shortcut, |
156 | child: Text(MenuEntry.colorGreen.label), |
157 | ), |
158 | MenuItemButton( |
159 | onPressed: () => _activate(MenuEntry.colorBlue), |
160 | shortcut: MenuEntry.colorBlue.shortcut, |
161 | child: Text(MenuEntry.colorBlue.label), |
162 | ), |
163 | ], |
164 | child: const Text('Background Color' ), |
165 | ), |
166 | ], |
167 | child: Container( |
168 | alignment: Alignment.center, |
169 | color: backgroundColor, |
170 | child: Column( |
171 | mainAxisAlignment: MainAxisAlignment.center, |
172 | children: <Widget>[ |
173 | const Padding( |
174 | padding: EdgeInsets.all(8.0), |
175 | child: Text('Right-click anywhere on the background to show the menu.' ), |
176 | ), |
177 | Padding( |
178 | padding: const EdgeInsets.all(12.0), |
179 | child: Text( |
180 | showingMessage ? widget.message : '' , |
181 | style: Theme.of(context).textTheme.headlineSmall, |
182 | ), |
183 | ), |
184 | Text(_lastSelection != null ? 'Last Selected: ${_lastSelection!.label}' : '' ), |
185 | ], |
186 | ), |
187 | ), |
188 | ), |
189 | ), |
190 | ); |
191 | } |
192 | |
193 | void _activate(MenuEntry selection) { |
194 | setState(() { |
195 | _lastSelection = selection; |
196 | }); |
197 | switch (selection) { |
198 | case MenuEntry.about: |
199 | showAboutDialog( |
200 | context: context, |
201 | applicationName: 'MenuBar Sample' , |
202 | applicationVersion: '1.0.0' , |
203 | ); |
204 | case MenuEntry.showMessage: |
205 | case MenuEntry.hideMessage: |
206 | showingMessage = !showingMessage; |
207 | case MenuEntry.colorMenu: |
208 | break; |
209 | case MenuEntry.colorRed: |
210 | backgroundColor = Colors.red; |
211 | case MenuEntry.colorGreen: |
212 | backgroundColor = Colors.green; |
213 | case MenuEntry.colorBlue: |
214 | backgroundColor = Colors.blue; |
215 | } |
216 | } |
217 | |
218 | void _handleSecondaryTapDown(TapDownDetails details) { |
219 | _menuController.open(position: details.localPosition); |
220 | } |
221 | |
222 | void _handleTapDown(TapDownDetails details) { |
223 | if (_menuController.isOpen) { |
224 | _menuController.close(); |
225 | return; |
226 | } |
227 | switch (defaultTargetPlatform) { |
228 | case TargetPlatform.android: |
229 | case TargetPlatform.fuchsia: |
230 | case TargetPlatform.linux: |
231 | case TargetPlatform.windows: |
232 | // Don't open the menu on these platforms with a Ctrl-tap (or a |
233 | // tap). |
234 | break; |
235 | case TargetPlatform.iOS: |
236 | case TargetPlatform.macOS: |
237 | // Only open the menu on these platforms if the control button is down |
238 | // when the tap occurs. |
239 | if (HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) || |
240 | HardwareKeyboard.instance.logicalKeysPressed.contains(LogicalKeyboardKey.controlRight)) { |
241 | _menuController.open(position: details.localPosition); |
242 | } |
243 | } |
244 | } |
245 | } |
246 | |
247 | class ContextMenuApp extends StatelessWidget { |
248 | const ContextMenuApp({super.key}); |
249 | |
250 | static const String kMessage = '"Talk less. Smile more." - A. Burr' ; |
251 | |
252 | @override |
253 | Widget build(BuildContext context) { |
254 | return MaterialApp( |
255 | theme: ThemeData(useMaterial3: true), |
256 | home: const Scaffold(body: MyContextMenu(message: kMessage)), |
257 | ); |
258 | } |
259 | } |
260 | |