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/services.dart';
6library;
7
8import 'framework.dart';
9import 'inherited_theme.dart';
10import 'navigator.dart';
11import 'overlay.dart';
12
13/// Builds and manages a context menu at a given location.
14///
15/// There can only ever be one context menu shown at a given time in the entire
16/// app. Calling [show] on one instance of this class will hide any other
17/// shown instances.
18///
19/// {@tool dartpad}
20/// This example shows how to use a GestureDetector to show a context menu
21/// anywhere in a widget subtree that receives a right click or long press.
22///
23/// ** See code in examples/api/lib/material/context_menu/context_menu_controller.0.dart **
24/// {@end-tool}
25///
26/// See also:
27///
28/// * [BrowserContextMenu], which allows the browser's context menu on web to
29/// be disabled and Flutter-rendered context menus to appear.
30class ContextMenuController {
31 /// Creates a context menu that can be shown with [show].
32 ContextMenuController({this.onRemove});
33
34 /// Called when this menu is removed.
35 final VoidCallback? onRemove;
36
37 /// The currently shown instance, if any.
38 static ContextMenuController? _shownInstance;
39
40 // The OverlayEntry is static because only one context menu can be displayed
41 // at one time.
42 static OverlayEntry? _menuOverlayEntry;
43
44 /// Shows the given context menu.
45 ///
46 /// Since there can only be one shown context menu at a time, calling this
47 /// will also remove any other context menu that is visible.
48 void show({
49 required BuildContext context,
50 required WidgetBuilder contextMenuBuilder,
51 Widget? debugRequiredFor,
52 }) {
53 removeAny();
54 final OverlayState overlayState = Overlay.of(
55 context,
56 rootOverlay: true,
57 debugRequiredFor: debugRequiredFor,
58 );
59 final CapturedThemes capturedThemes = InheritedTheme.capture(
60 from: context,
61 to: Navigator.maybeOf(context)?.context,
62 );
63
64 _menuOverlayEntry = OverlayEntry(
65 builder: (BuildContext context) {
66 return capturedThemes.wrap(contextMenuBuilder(context));
67 },
68 );
69 overlayState.insert(_menuOverlayEntry!);
70 _shownInstance = this;
71 }
72
73 /// Remove the currently shown context menu from the UI.
74 ///
75 /// Does nothing if no context menu is currently shown.
76 ///
77 /// If a menu is removed, and that menu provided an [onRemove] callback when
78 /// it was created, then that callback will be called.
79 ///
80 /// See also:
81 ///
82 /// * [remove], which removes only the current instance.
83 static void removeAny() {
84 _menuOverlayEntry?.remove();
85 _menuOverlayEntry?.dispose();
86 _menuOverlayEntry = null;
87 if (_shownInstance != null) {
88 _shownInstance!.onRemove?.call();
89 _shownInstance = null;
90 }
91 }
92
93 /// True if and only if this menu is currently being shown.
94 bool get isShown => _shownInstance == this;
95
96 /// Cause the underlying [OverlayEntry] to rebuild during the next pipeline
97 /// flush.
98 ///
99 /// It's necessary to call this function if the output of `contextMenuBuilder`
100 /// has changed.
101 ///
102 /// Errors if the context menu is not currently shown.
103 ///
104 /// See also:
105 ///
106 /// * [OverlayEntry.markNeedsBuild]
107 void markNeedsBuild() {
108 assert(isShown);
109 _menuOverlayEntry?.markNeedsBuild();
110 }
111
112 /// Remove this menu from the UI.
113 ///
114 /// Does nothing if this instance is not currently shown. In other words, if
115 /// another context menu is currently shown, that menu will not be removed.
116 ///
117 /// This method should only be called once. The instance cannot be shown again
118 /// after removing. Create a new instance.
119 ///
120 /// If an [onRemove] method was given to this instance, it will be called.
121 ///
122 /// See also:
123 ///
124 /// * [removeAny], which removes any shown instance of the context menu.
125 void remove() {
126 if (!isShown) {
127 return;
128 }
129 removeAny();
130 }
131}
132