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/// @docImport 'package:flutter/material.dart';
6library;
7
8import 'dart:ui' as ui;
9
10import 'package:flutter/foundation.dart';
11import 'package:flutter/rendering.dart';
12import 'package:flutter/scheduler.dart';
13import 'package:flutter/services.dart';
14
15import 'actions.dart';
16import 'basic.dart';
17import 'focus_manager.dart';
18import 'focus_traversal.dart';
19import 'framework.dart';
20import 'media_query.dart';
21import 'overlay.dart';
22import 'scroll_position.dart';
23import 'scrollable.dart';
24import 'shortcuts.dart';
25import 'tap_region.dart';
26
27// Examples can assume:
28// late BuildContext context;
29// late List menuItems;
30// late RawMenuOverlayInfo info;
31
32const bool _kDebugMenus = false;
33
34const Map<ShortcutActivator, Intent> _kMenuTraversalShortcuts = <ShortcutActivator, Intent>{
35 SingleActivator(LogicalKeyboardKey.gameButtonA): ActivateIntent(),
36 SingleActivator(LogicalKeyboardKey.escape): DismissIntent(),
37 SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
38 SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),
39 SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left),
40 SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right),
41};
42
43/// Anchor and menu information passed to [RawMenuAnchor].
44@immutable
45class RawMenuOverlayInfo {
46 /// Creates a [RawMenuOverlayInfo].
47 const RawMenuOverlayInfo({
48 required this.anchorRect,
49 required this.overlaySize,
50 required this.tapRegionGroupId,
51 this.position,
52 });
53
54 /// The position of the anchor widget that the menu is attached to, relative to
55 /// the nearest ancestor [Overlay] when [RawMenuAnchor.useRootOverlay] is false,
56 /// or the root [Overlay] when [RawMenuAnchor.useRootOverlay] is true.
57 final ui.Rect anchorRect;
58
59 /// The [Size] of the overlay that the menu is being shown in.
60 final ui.Size overlaySize;
61
62 /// The `position` argument passed to [MenuController.open].
63 ///
64 /// The position should be used to offset the menu relative to the top-left
65 /// corner of the anchor.
66 final Offset? position;
67
68 /// The [TapRegion.groupId] of the [TapRegion] that wraps widgets in this menu
69 /// system.
70 final Object tapRegionGroupId;
71
72 @override
73 bool operator ==(Object other) {
74 if (identical(this, other)) {
75 return true;
76 }
77
78 if (other.runtimeType != runtimeType) {
79 return false;
80 }
81
82 return other is RawMenuOverlayInfo &&
83 other.anchorRect == anchorRect &&
84 other.overlaySize == overlaySize &&
85 other.position == position &&
86 other.tapRegionGroupId == tapRegionGroupId;
87 }
88
89 @override
90 int get hashCode {
91 return Object.hash(anchorRect, overlaySize, position, tapRegionGroupId);
92 }
93}
94
95/// Signature for the builder function used by [RawMenuAnchor.overlayBuilder] to
96/// build a menu's overlay.
97///
98/// The `context` is the context that the overlay is being built in.
99///
100/// The `info` describes the anchor's [Rect], the [Size] of the overlay,
101/// the [TapRegion.groupId] used by members of the menu system, and the
102/// `position` argument passed to [MenuController.open].
103typedef RawMenuAnchorOverlayBuilder =
104 Widget Function(BuildContext context, RawMenuOverlayInfo info);
105
106/// Signature for the builder function used by [RawMenuAnchor.builder] to build
107/// the widget that the [RawMenuAnchor] surrounds.
108///
109/// The `context` is the context in which the anchor is being built.
110///
111/// The `controller` is the [MenuController] that can be used to open and close
112/// the menu.
113///
114/// The `child` is an optional child supplied as the [RawMenuAnchor.child]
115/// attribute. The child is intended to be incorporated in the result of the
116/// function.
117typedef RawMenuAnchorChildBuilder =
118 Widget Function(BuildContext context, MenuController controller, Widget? child);
119
120// An [InheritedWidget] used to notify anchor descendants when a menu opens
121// and closes, and to pass the anchor's controller to descendants.
122class _MenuControllerScope extends InheritedWidget {
123 const _MenuControllerScope({
124 required this.isOpen,
125 required this.controller,
126 required super.child,
127 });
128
129 final bool isOpen;
130 final MenuController controller;
131
132 @override
133 bool updateShouldNotify(_MenuControllerScope oldWidget) {
134 return isOpen != oldWidget.isOpen;
135 }
136}
137
138/// A widget that wraps a child and anchors a floating menu.
139///
140/// The child can be any widget, but is typically a button, a text field, or, in
141/// the case of context menus, the entire screen.
142///
143/// The menu overlay of a [RawMenuAnchor] is shown by calling
144/// [MenuController.open] on an attached [MenuController].
145///
146/// When a [RawMenuAnchor] is opened, [overlayBuilder] is called to construct
147/// the menu contents within an [Overlay]. The [Overlay] allows the menu to
148/// "float" on top of other widgets. The `info` argument passed to
149/// [overlayBuilder] provides the anchor's [Rect], the [Size] of the overlay,
150/// the [TapRegion.groupId] used by members of the menu system, and the
151/// `position` argument passed to [MenuController.open].
152///
153/// If [MenuController.open] is called with a `position` argument, it will be
154/// passed to the `info` argument of the `overlayBuilder` function.
155///
156/// Users are responsible for managing the positioning, semantics, and focus of
157/// the menu.
158///
159/// {@tool dartpad}
160///
161/// This example uses a [RawMenuAnchor] to build a basic select menu with
162/// four items.
163///
164/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.0.dart **
165/// {@end-tool}
166class RawMenuAnchor extends StatefulWidget {
167 /// A [RawMenuAnchor] that delegates overlay construction to an [overlayBuilder].
168 ///
169 /// The [overlayBuilder] should not be null.
170 const RawMenuAnchor({
171 super.key,
172 this.childFocusNode,
173 this.consumeOutsideTaps = false,
174 this.onOpen,
175 this.onClose,
176 this.useRootOverlay = false,
177 this.builder,
178 required this.controller,
179 required this.overlayBuilder,
180 this.child,
181 });
182
183 /// A callback that is invoked when the menu is opened.
184 final VoidCallback? onOpen;
185
186 /// A callback that is invoked when the menu is closed.
187 final VoidCallback? onClose;
188
189 /// A builder that builds the widget that this [RawMenuAnchor] surrounds.
190 ///
191 /// Typically, this is a button used to open the menu by calling
192 /// [MenuController.open] on the `controller` passed to the builder.
193 ///
194 /// If not supplied, then the [RawMenuAnchor] will be the size that its parent
195 /// allocates for it.
196 final RawMenuAnchorChildBuilder? builder;
197
198 /// The optional child to be passed to the [builder].
199 ///
200 /// Supply this child if there is a portion of the widget tree built in
201 /// [builder] that doesn't depend on the `controller` or `context` supplied to
202 /// the [builder]. It will be more efficient, since Flutter doesn't then need
203 /// to rebuild this child when those change.
204 final Widget? child;
205
206 /// The [overlayBuilder] function is passed a [RawMenuOverlayInfo] object that
207 /// defines the anchor's [Rect], the [Size] of the overlay, the
208 /// [TapRegion.groupId] for the menu system, and the position [Offset] passed
209 /// to [MenuController.open].
210 ///
211 /// To ensure taps are properly consumed, the
212 /// [RawMenuOverlayInfo.tapRegionGroupId] should be passed to a [TapRegion]
213 /// widget that wraps the menu panel.
214 ///
215 /// ```dart
216 /// TapRegion(
217 /// groupId: info.tapRegionGroupId,
218 /// onTapOutside: (PointerDownEvent event) {
219 /// MenuController.maybeOf(context)?.close();
220 /// },
221 /// child: Column(children: menuItems),
222 /// )
223 /// ```
224 final RawMenuAnchorOverlayBuilder overlayBuilder;
225
226 /// {@template flutter.widgets.RawMenuAnchor.useRootOverlay}
227 /// Whether the menu panel should be rendered in the root [Overlay].
228 ///
229 /// When true, the menu is mounted in the root overlay. Rendering the menu in
230 /// the root overlay prevents the menu from being obscured by other widgets.
231 ///
232 /// When false, the menu is rendered in the nearest ancestor [Overlay].
233 ///
234 /// Submenus will always use the same overlay as their top-level ancestor, so
235 /// setting a [useRootOverlay] value on a submenu will have no effect.
236 /// {@endtemplate}
237 ///
238 /// Defaults to false on overlay menus.
239 final bool useRootOverlay;
240
241 /// The [FocusNode] attached to the widget that takes focus when the
242 /// menu is opened or closed.
243 ///
244 /// If not supplied, the anchor will not retain focus when the menu is opened.
245 final FocusNode? childFocusNode;
246
247 /// Whether or not a tap event that closes the menu will be permitted to
248 /// continue on to the gesture arena.
249 ///
250 /// If false, then tapping outside of a menu when the menu is open will both
251 /// close the menu, and allow the tap to participate in the gesture arena.
252 ///
253 /// If true, then it will only close the menu, and the tap event will be
254 /// consumed.
255 ///
256 /// Defaults to false.
257 final bool consumeOutsideTaps;
258
259 /// A [MenuController] that allows opening and closing of the menu from other
260 /// widgets.
261 final MenuController controller;
262
263 @override
264 State<RawMenuAnchor> createState() => _RawMenuAnchorState();
265
266 @override
267 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
268 super.debugFillProperties(properties);
269 properties.add(ObjectFlagProperty<FocusNode>.has('focusNode', childFocusNode));
270 properties.add(
271 FlagProperty(
272 'useRootOverlay',
273 value: useRootOverlay,
274 ifFalse: 'use nearest overlay',
275 ifTrue: 'use root overlay',
276 ),
277 );
278 }
279}
280
281// Base mixin that provides the common interface and state for both types of
282// [RawMenuAnchor]s, [RawMenuAnchor] and [RawMenuAnchorGroup].
283@optionalTypeArgs
284mixin _RawMenuAnchorBaseMixin<T extends StatefulWidget> on State<T> {
285 final List<_RawMenuAnchorBaseMixin> _anchorChildren = <_RawMenuAnchorBaseMixin>[];
286 _RawMenuAnchorBaseMixin? _parent;
287 ScrollPosition? _scrollPosition;
288 Size? _viewSize;
289
290 /// Whether this [_RawMenuAnchorBaseMixin] is the top node of the menu tree.
291 @protected
292 bool get isRoot => _parent == null;
293
294 /// The [MenuController] that is used by the [_RawMenuAnchorBaseMixin].
295 ///
296 /// If an overridding widget does not provide a [MenuController], then
297 /// [_RawMenuAnchorBaseMixin] will create and manage its own.
298 MenuController get menuController;
299
300 /// Whether this submenu's overlay is visible.
301 @protected
302 bool get isOpen;
303
304 /// The root of the menu tree that this [RawMenuAnchor] is in.
305 @protected
306 _RawMenuAnchorBaseMixin get root {
307 _RawMenuAnchorBaseMixin anchor = this;
308 while (anchor._parent != null) {
309 anchor = anchor._parent!;
310 }
311 return anchor;
312 }
313
314 @override
315 void initState() {
316 super.initState();
317 menuController._attach(this);
318 }
319
320 @override
321 void didChangeDependencies() {
322 super.didChangeDependencies();
323 final _RawMenuAnchorBaseMixin? newParent = MenuController.maybeOf(context)?._anchor;
324 if (newParent != _parent) {
325 assert(
326 newParent != this,
327 'A MenuController should only be attached to one anchor at a time.',
328 );
329 _parent?._removeChild(this);
330 _parent = newParent;
331 _parent?._addChild(this);
332 }
333
334 _scrollPosition?.isScrollingNotifier.removeListener(_handleScroll);
335 _scrollPosition = Scrollable.maybeOf(context)?.position;
336 _scrollPosition?.isScrollingNotifier.addListener(_handleScroll);
337 final Size newSize = MediaQuery.sizeOf(context);
338 if (_viewSize != null && newSize != _viewSize) {
339 // Close the menus if the view changes size.
340 root.close();
341 }
342 _viewSize = newSize;
343 }
344
345 @override
346 void dispose() {
347 assert(_debugMenuInfo('Disposing of $this'));
348 if (isOpen) {
349 close(inDispose: true);
350 }
351
352 _parent?._removeChild(this);
353 _parent = null;
354 _anchorChildren.clear();
355 menuController._detach(this);
356 super.dispose();
357 }
358
359 void _addChild(_RawMenuAnchorBaseMixin child) {
360 assert(isRoot || _debugMenuInfo('Added root child: $child'));
361 assert(!_anchorChildren.contains(child));
362 _anchorChildren.add(child);
363 assert(_debugMenuInfo('Added:\n${child.widget.toStringDeep()}'));
364 assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
365 }
366
367 void _removeChild(_RawMenuAnchorBaseMixin child) {
368 assert(isRoot || _debugMenuInfo('Removed root child: $child'));
369 assert(_anchorChildren.contains(child));
370 assert(_debugMenuInfo('Removing:\n${child.widget.toStringDeep()}'));
371 _anchorChildren.remove(child);
372 assert(_debugMenuInfo('Tree:\n${widget.toStringDeep()}'));
373 }
374
375 void _handleScroll() {
376 // If an ancestor scrolls, and we're a root anchor, then close the menus.
377 // Don't just close it on *any* scroll, since we want to be able to scroll
378 // menus themselves if they're too big for the view.
379 if (isRoot) {
380 close();
381 }
382 }
383
384 void _childChangedOpenState() {
385 _parent?._childChangedOpenState();
386 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
387 setState(() {
388 // Mark dirty now, but only if not in a build.
389 });
390 } else {
391 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
392 setState(() {
393 // Mark dirty
394 });
395 });
396 }
397 }
398
399 /// Open the menu, optionally at a position relative to the [RawMenuAnchor].
400 ///
401 /// Call this when the menu should be shown to the user.
402 ///
403 /// The optional `position` argument should specify the location of the menu in
404 /// the local coordinates of the [RawMenuAnchor].
405 @protected
406 void open({Offset? position});
407
408 /// Close the menu.
409 @protected
410 void close({bool inDispose = false});
411
412 @protected
413 void closeChildren({bool inDispose = false}) {
414 assert(_debugMenuInfo('Closing children of $this${inDispose ? ' (dispose)' : ''}'));
415 for (final _RawMenuAnchorBaseMixin child in List<_RawMenuAnchorBaseMixin>.from(
416 _anchorChildren,
417 )) {
418 child.close(inDispose: inDispose);
419 }
420 }
421
422 /// Handles taps outside of the menu surface.
423 ///
424 /// By default, this closes this submenu's children.
425 @protected
426 void handleOutsideTap(PointerDownEvent pointerDownEvent) {
427 assert(_debugMenuInfo('Tapped Outside $menuController'));
428 closeChildren();
429 }
430
431 // Used to build the anchor widget in subclasses.
432 @protected
433 Widget buildAnchor(BuildContext context);
434
435 @override
436 @nonVirtual
437 Widget build(BuildContext context) {
438 return _MenuControllerScope(
439 isOpen: isOpen,
440 controller: menuController,
441 child: Actions(
442 actions: <Type, Action<Intent>>{
443 // Check if open to allow DismissIntent to bubble when the menu is
444 // closed.
445 if (isOpen) DismissIntent: DismissMenuAction(controller: menuController),
446 },
447 child: Builder(builder: buildAnchor),
448 ),
449 );
450 }
451
452 @override
453 String toString({DiagnosticLevel? minLevel}) => describeIdentity(this);
454}
455
456class _RawMenuAnchorState extends State<RawMenuAnchor> with _RawMenuAnchorBaseMixin<RawMenuAnchor> {
457 // This is the global key that is used later to determine the bounding rect
458 // for the anchor's region that the CustomSingleChildLayout's delegate
459 // uses to determine where to place the menu on the screen and to avoid the
460 // view's edges.
461 final GlobalKey _anchorKey = GlobalKey<_RawMenuAnchorState>(
462 debugLabel: kReleaseMode ? null : 'MenuAnchor',
463 );
464 final OverlayPortalController _overlayController = OverlayPortalController(
465 debugLabel: kReleaseMode ? null : 'MenuAnchor controller',
466 );
467
468 Offset? _menuPosition;
469 bool get _isRootOverlayAnchor => _parent is! _RawMenuAnchorState;
470
471 // If we are a nested menu, we still want to use the same overlay as the
472 // root menu.
473 bool get useRootOverlay {
474 if (_parent case _RawMenuAnchorState(useRootOverlay: final bool useRoot)) {
475 return useRoot;
476 }
477
478 assert(_isRootOverlayAnchor);
479 return widget.useRootOverlay;
480 }
481
482 @override
483 bool get isOpen => _overlayController.isShowing;
484
485 @override
486 MenuController get menuController => widget.controller;
487
488 @override
489 void didUpdateWidget(RawMenuAnchor oldWidget) {
490 super.didUpdateWidget(oldWidget);
491 if (oldWidget.controller != widget.controller) {
492 oldWidget.controller._detach(this);
493 widget.controller._attach(this);
494 }
495 }
496
497 @override
498 void open({Offset? position}) {
499 assert(menuController._anchor == this);
500 if (isOpen) {
501 if (position == _menuPosition) {
502 assert(_debugMenuInfo("Not opening $this because it's already open"));
503 // The menu is open and not being moved, so just return.
504 return;
505 }
506
507 // The menu is already open, but we need to move to another location, so
508 // close it first.
509 close();
510 }
511
512 assert(_debugMenuInfo('Opening $this at ${position ?? Offset.zero}'));
513
514 // Close all siblings.
515 _parent?.closeChildren();
516 assert(!_overlayController.isShowing);
517
518 _parent?._childChangedOpenState();
519 _menuPosition = position;
520 _overlayController.show();
521
522 if (_isRootOverlayAnchor) {
523 widget.childFocusNode?.requestFocus();
524 }
525
526 widget.onOpen?.call();
527 if (mounted && SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
528 setState(() {
529 // Mark dirty to notify MenuController dependents.
530 });
531 }
532 }
533
534 // Close the menu.
535 //
536 // Call this when the menu should be closed. Has no effect if the menu is
537 // already closed.
538 @override
539 void close({bool inDispose = false}) {
540 assert(_debugMenuInfo('Closing $this'));
541 if (!isOpen) {
542 return;
543 }
544
545 closeChildren(inDispose: inDispose);
546 // Don't hide if we're in the middle of a build.
547 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
548 _overlayController.hide();
549 } else if (!inDispose) {
550 SchedulerBinding.instance.addPostFrameCallback((_) {
551 _overlayController.hide();
552 }, debugLabel: 'MenuAnchor.hide');
553 }
554
555 if (!inDispose) {
556 // Notify that _childIsOpen changed state, but only if not
557 // currently disposing.
558 _parent?._childChangedOpenState();
559 widget.onClose?.call();
560 if (mounted &&
561 SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
562 setState(() {
563 // Mark dirty, but only if mounted and not in a build.
564 });
565 }
566 }
567 }
568
569 Widget _buildOverlay(BuildContext context) {
570 final BuildContext anchorContext = _anchorKey.currentContext!;
571 final RenderBox overlay =
572 Overlay.of(anchorContext, rootOverlay: useRootOverlay).context.findRenderObject()!
573 as RenderBox;
574 final RenderBox anchorBox = anchorContext.findRenderObject()! as RenderBox;
575 final ui.Offset upperLeft = anchorBox.localToGlobal(Offset.zero, ancestor: overlay);
576 final ui.Offset bottomRight = anchorBox.localToGlobal(
577 anchorBox.size.bottomRight(Offset.zero),
578 ancestor: overlay,
579 );
580
581 final RawMenuOverlayInfo info = RawMenuOverlayInfo(
582 anchorRect: Rect.fromPoints(upperLeft, bottomRight),
583 overlaySize: overlay.size,
584 position: _menuPosition,
585 tapRegionGroupId: root.menuController,
586 );
587
588 return widget.overlayBuilder(context, info);
589 }
590
591 @override
592 Widget buildAnchor(BuildContext context) {
593 final Widget child = Shortcuts(
594 includeSemantics: false,
595 shortcuts: _kMenuTraversalShortcuts,
596 child: TapRegion(
597 groupId: root.menuController,
598 consumeOutsideTaps: root.isOpen && widget.consumeOutsideTaps,
599 onTapOutside: handleOutsideTap,
600 child: Builder(
601 key: _anchorKey,
602 builder: (BuildContext context) {
603 return widget.builder?.call(context, menuController, widget.child) ??
604 widget.child ??
605 const SizedBox();
606 },
607 ),
608 ),
609 );
610
611 if (useRootOverlay) {
612 return OverlayPortal.targetsRootOverlay(
613 controller: _overlayController,
614 overlayChildBuilder: _buildOverlay,
615 child: child,
616 );
617 } else {
618 return OverlayPortal(
619 controller: _overlayController,
620 overlayChildBuilder: _buildOverlay,
621 child: child,
622 );
623 }
624 }
625
626 @override
627 String toString({DiagnosticLevel? minLevel}) {
628 return describeIdentity(this);
629 }
630}
631
632/// Creates a menu anchor that is always visible and is not displayed in an
633/// [OverlayPortal].
634///
635/// A [RawMenuAnchorGroup] can be used to create a menu bar that handles
636/// external taps and keyboard shortcuts, but defines no default focus or
637/// keyboard traversal to enable more flexibility.
638///
639/// When a [MenuController] is given to a [RawMenuAnchorGroup],
640/// - [MenuController.open] has no effect.
641/// - [MenuController.close] closes all child [RawMenuAnchor]s that are open
642/// - [MenuController.isOpen] reflects whether any child [RawMenuAnchor] is
643/// open.
644///
645/// A [child] must be provided.
646///
647/// {@tool dartpad}
648///
649/// This example uses [RawMenuAnchorGroup] to build a menu bar with four
650/// submenus. Hovering over a menu item opens its respective submenu. Selecting
651/// a menu item will close the menu and update the selected item text.
652///
653/// ** See code in examples/api/lib/widgets/raw_menu_anchor/raw_menu_anchor.1.dart **
654/// {@end-tool}
655///
656/// See also:
657/// * [MenuBar], which wraps this widget with standard layout and semantics and
658/// focus management.
659/// * [MenuAnchor], a menu anchor that follows the Material Design guidelines.
660/// * [RawMenuAnchor], a widget that defines a region attached to a floating
661/// submenu.
662class RawMenuAnchorGroup extends StatefulWidget {
663 /// Creates a [RawMenuAnchorGroup].
664 const RawMenuAnchorGroup({super.key, required this.child, required this.controller});
665
666 /// The child displayed by the [RawMenuAnchorGroup].
667 ///
668 /// To access the [MenuController] from the [child], place the child in a
669 /// builder and call [MenuController.maybeOf].
670 final Widget child;
671
672 /// An [MenuController] that allows the closing of the menu from other
673 /// widgets.
674 final MenuController controller;
675
676 @override
677 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
678 super.debugFillProperties(properties);
679 properties.add(ObjectFlagProperty<MenuController>.has('controller', controller));
680 }
681
682 @override
683 State<RawMenuAnchorGroup> createState() => _RawMenuAnchorGroupState();
684}
685
686class _RawMenuAnchorGroupState extends State<RawMenuAnchorGroup>
687 with _RawMenuAnchorBaseMixin<RawMenuAnchorGroup> {
688 @override
689 bool get isOpen => _anchorChildren.any((_RawMenuAnchorBaseMixin child) => child.isOpen);
690
691 @override
692 MenuController get menuController => widget.controller;
693
694 @override
695 void didUpdateWidget(RawMenuAnchorGroup oldWidget) {
696 super.didUpdateWidget(oldWidget);
697 if (oldWidget.controller != widget.controller) {
698 oldWidget.controller._detach(this);
699 widget.controller._attach(this);
700 }
701 }
702
703 @override
704 void close({bool inDispose = false}) {
705 if (!isOpen) {
706 return;
707 }
708
709 closeChildren(inDispose: inDispose);
710 if (!inDispose) {
711 if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
712 setState(() {
713 // Mark dirty, but only if mounted and not in a build.
714 });
715 } else {
716 SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
717 if (mounted) {
718 setState(() {
719 // Mark dirty.
720 });
721 }
722 });
723 }
724 }
725 }
726
727 @override
728 void open({Offset? position}) {
729 assert(menuController._anchor == this);
730 // Menu nodes are always open, so this is a no-op.
731 return;
732 }
733
734 @override
735 Widget buildAnchor(BuildContext context) {
736 return TapRegion(
737 groupId: root.menuController,
738 onTapOutside: handleOutsideTap,
739 child: widget.child,
740 );
741 }
742}
743
744/// A controller used to manage a menu created by a [RawMenuAnchor], or
745/// [RawMenuAnchorGroup].
746///
747/// A [MenuController] is used to control and interrogate a menu after it has
748/// been created, with methods such as [open] and [close], and state accessors
749/// like [isOpen].
750///
751/// [MenuController.maybeOf] can be used to retrieve a controller from the
752/// [BuildContext] of a widget that is a descendant of a [MenuAnchor],
753/// [MenuBar], [SubmenuButton], or [RawMenuAnchor]. Doing so will not establish
754/// a dependency relationship.
755///
756/// [MenuController.maybeIsOpenOf] can be used to interrogate the state of a
757/// menu from the [BuildContext] of a widget that is a descendant of a
758/// [MenuAnchor]. Unlike [MenuController.maybeOf], this method will establish a
759/// dependency relationship, so the calling widget will rebuild when the menu
760/// opens and closes, and when the [MenuController] changes.
761///
762/// See also:
763///
764/// * [MenuAnchor], a menu anchor that follows the Material Design guidelines.
765/// * [MenuBar], a widget that creates a menu bar that can take an optional
766/// [MenuController].
767/// * [SubmenuButton], a Material widget that has a button that manages a
768/// submenu.
769/// * [RawMenuAnchor], a generic widget that manages a submenu.
770/// * [RawMenuAnchorGroup], a generic widget that wraps a group of submenus.
771class MenuController {
772 /// The anchor that this controller controls.
773 ///
774 /// This is set automatically when a [MenuController] is given to the anchor
775 /// it controls.
776 _RawMenuAnchorBaseMixin? _anchor;
777
778 /// Whether or not the menu associated with this [MenuController] is open.
779 bool get isOpen => _anchor?.isOpen ?? false;
780
781 /// Opens the menu that this [MenuController] is associated with.
782 ///
783 /// If `position` is given, then the menu will open at the position given, in
784 /// the coordinate space of the root overlay.
785 ///
786 /// If the menu's anchor point is scrolled by an ancestor, or the view changes
787 /// size, then any open menus will automatically close.
788 void open({Offset? position}) {
789 assert(_anchor != null);
790 _anchor!.open(position: position);
791 }
792
793 /// Close the menu that this [MenuController] is associated with.
794 ///
795 /// Associating with a menu is done by passing a [MenuController] to a
796 /// [MenuAnchor], [RawMenuAnchor], or [RawMenuAnchorGroup].
797 ///
798 /// If the menu's anchor point is scrolled by an ancestor, or the view changes
799 /// size, then any open menu will automatically close.
800 void close() {
801 _anchor?.close();
802 }
803
804 /// Close the children of the menu associated with this [MenuController],
805 /// without closing the menu itself.
806 void closeChildren() {
807 assert(_anchor != null);
808 _anchor!.closeChildren();
809 }
810
811 // ignore: use_setters_to_change_properties
812 void _attach(_RawMenuAnchorBaseMixin anchor) {
813 _anchor = anchor;
814 }
815
816 void _detach(_RawMenuAnchorBaseMixin anchor) {
817 if (_anchor == anchor) {
818 _anchor = null;
819 }
820 }
821
822 /// Returns the [MenuController] of the ancestor [RawMenuAnchor] or
823 /// [RawMenuAnchorGroup] nearest to the given `context`, if one exists.
824 /// Otherwise, returns null.
825 ///
826 /// This method will not establish a dependency relationship, so the calling
827 /// widget will not rebuild when the menu opens and closes, nor when the
828 /// [MenuController] changes.
829 static MenuController? maybeOf(BuildContext context) {
830 return context.getInheritedWidgetOfExactType<_MenuControllerScope>()?.controller;
831 }
832
833 /// Returns the value of [MenuController.isOpen] of the ancestor
834 /// [RawMenuAnchor] or [RawMenuAnchorGroup] nearest to the given `context`, if
835 /// one exists. Otherwise, returns null.
836 ///
837 /// This method will establish a dependency relationship, so the calling
838 /// widget will rebuild when the menu opens and closes.
839 static bool? maybeIsOpenOf(BuildContext context) {
840 return context.dependOnInheritedWidgetOfExactType<_MenuControllerScope>()?.isOpen;
841 }
842
843 @override
844 String toString() => describeIdentity(this);
845}
846
847/// An action that closes all the menus associated with the given
848/// [MenuController].
849///
850/// See also:
851///
852/// * [MenuAnchor], a material-themed widget that hosts a cascading submenu.
853/// * [MenuBar], a widget that defines a menu bar with cascading submenus.
854/// * [RawMenuAnchor], a widget that hosts a cascading submenu.
855/// * [MenuController], a controller used to manage menus created by a
856/// [RawMenuAnchor].
857class DismissMenuAction extends DismissAction {
858 /// Creates a [DismissMenuAction].
859 DismissMenuAction({required this.controller});
860
861 /// The [MenuController] that manages the menu which should be dismissed upon
862 /// invocation.
863 final MenuController controller;
864
865 @override
866 void invoke(DismissIntent intent) {
867 controller._anchor!.root.close();
868 }
869
870 @override
871 bool isEnabled(DismissIntent intent) {
872 return controller._anchor != null;
873 }
874}
875
876/// A debug print function, which should only be called within an assert, like
877/// so:
878///
879/// assert(_debugMenuInfo('Debug Message'));
880///
881/// so that the call is entirely removed in release builds.
882///
883/// Enable debug printing by setting [_kDebugMenus] to true at the top of the
884/// file.
885bool _debugMenuInfo(String message, [Iterable<String>? details]) {
886 assert(() {
887 if (_kDebugMenus) {
888 debugPrint('MENU: $message');
889 if (details != null && details.isNotEmpty) {
890 for (final String detail in details) {
891 debugPrint(' $detail');
892 }
893 }
894 }
895 return true;
896 }());
897 // Return true so that it can be easily used inside of an assert.
898 return true;
899}
900

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com