Unverified Commit 2051f09c authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add `Overlay.maybeOf`, make `Overlay.of` return a non-nullable instance (#110811)

parent a7ba717b
......@@ -132,7 +132,7 @@ class _OverlayExampleState extends State<OverlayExample> {
);
// Add the OverlayEntry to the Overlay.
Overlay.of(context, debugRequiredFor: widget)!.insert(overlayEntry!);
Overlay.of(context, debugRequiredFor: widget).insert(overlayEntry!);
}
// Remove the OverlayEntry.
......
......@@ -363,7 +363,7 @@ class _CupertinoContextMenuState extends State<CupertinoContextMenu> with Ticker
);
},
);
Overlay.of(context, rootOverlay: true, debugRequiredFor: widget)!.insert(_lastOverlayEntry!);
Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(_lastOverlayEntry!);
_openController.forward();
}
......
......@@ -655,7 +655,7 @@ class _RangeSliderState extends State<RangeSlider> with TickerProviderStateMixin
);
},
);
Overlay.of(context, debugRequiredFor: widget)!.insert(overlayEntry!);
Overlay.of(context, debugRequiredFor: widget).insert(overlayEntry!);
}
}
}
......
......@@ -887,7 +887,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
);
},
);
Overlay.of(context, debugRequiredFor: widget)!.insert(overlayEntry!);
Overlay.of(context, debugRequiredFor: widget).insert(overlayEntry!);
}
}
}
......
......@@ -517,7 +517,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
)!;
);
overlayState.insert(_entry!);
}
SemanticsService.tooltip(_tooltipMessage);
......@@ -573,7 +573,7 @@ class TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
)!;
);
final RenderBox box = context.findRenderObject()! as RenderBox;
final Offset target = box.localToGlobal(
......
......@@ -438,7 +438,7 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
);
},
);
Overlay.of(context, rootOverlay: true, debugRequiredFor: widget)!.insert(newFloatingOptions);
Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(newFloatingOptions);
_floatingOptions = newFloatingOptions;
} else {
_floatingOptions = null;
......
......@@ -555,7 +555,7 @@ class _DraggableState<T extends Object> extends State<Draggable<T>> {
_activeCount += 1;
});
final _DragAvatar<T> avatar = _DragAvatar<T>(
overlayState: Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay)!,
overlayState: Overlay.of(context, debugRequiredFor: widget, rootOverlay: widget.rootOverlay),
data: widget.data,
axis: widget.axis,
initialPosition: position,
......
......@@ -168,7 +168,7 @@ class MagnifierController {
/// final MagnifierController myMagnifierController = MagnifierController();
///
/// // Placed below the magnifier, so it will show.
/// Overlay.of(context)!.insert(OverlayEntry(
/// Overlay.of(context).insert(OverlayEntry(
/// builder: (BuildContext context) => const Text('I WILL display in the magnifier')));
///
/// // Will display in the magnifier, since this entry was passed to show.
......@@ -176,7 +176,7 @@ class MagnifierController {
/// builder: (BuildContext context) =>
/// const Text('I WILL display in the magnifier'));
///
/// Overlay.of(context)!
/// Overlay.of(context)
/// .insert(displayInMagnifier);
/// myMagnifierController.show(
/// context: context,
......@@ -186,10 +186,10 @@ class MagnifierController {
/// ));
///
/// // By default, new entries will be placed over the top entry.
/// Overlay.of(context)!.insert(OverlayEntry(
/// Overlay.of(context).insert(OverlayEntry(
/// builder: (BuildContext context) => const Text('I WILL NOT display in the magnifier')));
///
/// Overlay.of(context)!.insert(
/// Overlay.of(context).insert(
/// below:
/// myMagnifierController.overlayEntry, // Explicitly placed below the magnifier.
/// OverlayEntry(
......@@ -246,7 +246,7 @@ class MagnifierController {
overlayEntry!.remove();
}
final OverlayState? overlayState = Overlay.of(
final OverlayState overlayState = Overlay.of(
context,
rootOverlay: true,
debugRequiredFor: debugRequiredFor,
......@@ -260,7 +260,7 @@ class MagnifierController {
_overlayEntry = OverlayEntry(
builder: (BuildContext context) => capturedThemes.wrap(builder(context)),
);
overlayState!.insert(overlayEntry!, below: below);
overlayState.insert(overlayEntry!, below: below);
if (animationController != null) {
await animationController?.forward();
......@@ -389,7 +389,7 @@ class MagnifierDecoration extends ShapeDecoration {
/// gesture, on an image or text.
/// {@endtemplate}
///
/// A magnifier can be convienently managed by [MagnifierController], which handles
/// A magnifier can be conveniently managed by [MagnifierController], which handles
/// showing and hiding the magnifier, with an optional entry / exit animation.
///
/// See:
......@@ -402,7 +402,7 @@ class RawMagnifier extends StatelessWidget {
/// the focal point is directly under the magnifier, and there is no magnification:
/// This means that a default magnifier will be entirely invisible to the naked eye,
/// since it is painting exactly what is under it, exactly where it was painted
/// orignally.
/// originally.
/// {@endtemplate}
const RawMagnifier({
super.key,
......@@ -414,7 +414,7 @@ class RawMagnifier extends StatelessWidget {
}) : assert(magnificationScale != 0,
'Magnification scale of 0 results in undefined behavior.');
/// An optional widget to posiiton inside the len of the [RawMagnifier].
/// An optional widget to position inside the len of the [RawMagnifier].
///
/// This is positioned over the [RawMagnifier] - it may be useful for tinting the
/// [RawMagnifier], or drawing a crosshair like UI.
......
......@@ -273,8 +273,9 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
/// [OverlayEntry] objects.
///
/// Although you can create an [Overlay] directly, it's most common to use the
/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The
/// navigator uses its overlay to manage the visual appearance of its routes.
/// overlay created by the [Navigator] in a [WidgetsApp], [CupertinoApp] or a
/// [MaterialApp]. The navigator uses its overlay to manage the visual
/// appearance of its routes.
///
/// The [Overlay] widget uses a custom stack implementation, which is very
/// similar to the [Stack] widget. The main use case of [Overlay] is related to
......@@ -298,6 +299,7 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
/// * [OverlayState], which is used to insert the entries into the overlay.
/// * [WidgetsApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [MaterialApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [CupertinoApp], which inserts an [Overlay] widget indirectly via its [Navigator].
/// * [Stack], which allows directly displaying a stack of widgets.
class Overlay extends StatefulWidget {
/// Creates an overlay.
......@@ -306,7 +308,8 @@ class Overlay extends StatefulWidget {
/// [OverlayState] is initialized.
///
/// Rather than creating an overlay, consider using the overlay that is
/// created by the [Navigator] in a [WidgetsApp] or a [MaterialApp] for the application.
/// created by the [Navigator] in a [WidgetsApp], [CupertinoApp], or a
/// [MaterialApp] for the application.
const Overlay({
super.key,
this.initialEntries = const <OverlayEntry>[],
......@@ -334,40 +337,46 @@ class Overlay extends StatefulWidget {
/// Defaults to [Clip.hardEdge], and must not be null.
final Clip clipBehavior;
/// The state from the closest instance of this class that encloses the given context.
/// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context, and, in debug mode, will throw if one is not found.
///
/// In debug mode, if the `debugRequiredFor` argument is provided then this
/// function will assert that an overlay was found and will throw an exception
/// if not. The exception attempts to explain that the calling [Widget] (the
/// one given by the `debugRequiredFor` argument) needs an [Overlay] to be
/// present to function.
/// In debug mode, if the `debugRequiredFor` argument is provided and an
/// overlay isn't found, then this function will throw an exception containing
/// the runtime type of the given widget in the error message. The exception
/// attempts to explain that the calling [Widget] (the one given by the
/// `debugRequiredFor` argument) needs an [Overlay] to be present to function.
/// If `debugRequiredFor` is not supplied, then the error message is more
/// generic.
///
/// Typical usage is as follows:
///
/// ```dart
/// OverlayState overlay = Overlay.of(context)!;
/// OverlayState overlay = Overlay.of(context);
/// ```
///
/// If `rootOverlay` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for installing overlay entries
/// above all subsequent instances of [Overlay].
/// this class is given instead. Useful for installing overlay entries above
/// all subsequent instances of [Overlay].
///
/// This method can be expensive (it walks the element tree).
static OverlayState? of(
///
/// See also:
///
/// * [Overlay.maybeOf] for a similar function that returns null if an
/// [Overlay] is not found.
static OverlayState of(
BuildContext context, {
bool rootOverlay = false,
Widget? debugRequiredFor,
}) {
final OverlayState? result = rootOverlay
? context.findRootAncestorStateOfType<OverlayState>()
: context.findAncestorStateOfType<OverlayState>();
final OverlayState? result = maybeOf(context, rootOverlay: rootOverlay);
assert(() {
if (debugRequiredFor != null && result == null) {
if (result == null) {
final List<DiagnosticsNode> information = <DiagnosticsNode>[
ErrorSummary('No Overlay widget found.'),
ErrorDescription('${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.'),
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.'),
DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
ErrorDescription('${debugRequiredFor?.runtimeType ?? 'Some'} widgets require an Overlay widget ancestor for correct operation.'),
ErrorHint('The most common way to add an Overlay to an application is to include a MaterialApp, CupertinoApp or Navigator widget in the runApp() call.'),
if (debugRequiredFor != null) DiagnosticsProperty<Widget>('The specific widget that failed to find an overlay was', debugRequiredFor, style: DiagnosticsTreeStyle.errorProperty),
if (context.widget != debugRequiredFor)
context.describeElement('The context from which that widget was searching for an overlay was'),
];
......@@ -376,7 +385,36 @@ class Overlay extends StatefulWidget {
}
return true;
}());
return result;
return result!;
}
/// The [OverlayState] from the closest instance of [Overlay] that encloses
/// the given context, if any.
///
/// Typical usage is as follows:
///
/// ```dart
/// OverlayState? overlay = Overlay.maybeOf(context);
/// ```
///
/// If `rootOverlay` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for installing overlay entries above
/// all subsequent instances of [Overlay].
///
/// This method can be expensive (it walks the element tree).
///
/// See also:
///
/// * [Overlay.of] for a similar function that returns a non-nullable result
/// and throws if an [Overlay] is not found.
static OverlayState? maybeOf(
BuildContext context, {
bool rootOverlay = false,
}) {
return rootOverlay
? context.findRootAncestorStateOfType<OverlayState>()
: context.findAncestorStateOfType<OverlayState>();
}
@override
......
......@@ -728,7 +728,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
);
_dragInfo!.startDrag();
final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget)!;
final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
assert(_overlayEntry == null);
_overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy);
overlay.insert(_overlayEntry!);
......@@ -923,7 +923,7 @@ class SliverReorderableListState extends State<SliverReorderableList> with Ticke
}
final Widget child = widget.itemBuilder(context, index);
assert(child.key != null, 'All list items must have a key');
final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget)!;
final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
return _ReorderableItem(
key: _ReorderableItemGlobalKey(child.key!, index, this),
index: index,
......@@ -1310,7 +1310,7 @@ class _DragInfo extends Drag {
}
Offset _overlayOrigin(BuildContext context) {
final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget)!;
final OverlayState overlay = Overlay.of(context, debugRequiredFor: context.widget);
final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox;
return overlayBox.localToGlobal(Offset.zero);
}
......
......@@ -120,7 +120,7 @@ abstract class TextSelectionControls {
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
// TODO(chunhtai): Change to ValueListenable<ClipboardStatus>? once
// mirgration is done. https://github.com/flutter/flutter/issues/99360
// migration is done. https://github.com/flutter/flutter/issues/99360
ClipboardStatusNotifier? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
);
......@@ -793,16 +793,16 @@ class SelectionOverlay {
/// a magnifierBuilder will not be provided, or the magnifierBuilder will return null
/// on platforms not mobile.
///
/// This is NOT the souce of truth for if the magnifier is up or not,
/// This is NOT the source of truth for if the magnifier is up or not,
/// since magnifiers may hide themselves. If this info is needed, check
/// [MagnifierController.shown].
void showMagnifier(MagnifierOverlayInfoBearer initalInfoBearer) {
void showMagnifier(MagnifierOverlayInfoBearer initialInfoBearer) {
if (_toolbar != null) {
hideToolbar();
}
// Start from empty, so we don't utilize any rememnant values.
_magnifierOverlayInfoBearer.value = initalInfoBearer;
// Start from empty, so we don't utilize any remnant values.
_magnifierOverlayInfoBearer.value = initialInfoBearer;
// Pre-build the magnifiers so we can tell if we've built something
// or not. If we don't build a magnifiers, then we should not
......@@ -1062,8 +1062,7 @@ class SelectionOverlay {
OverlayEntry(builder: _buildEndHandle),
];
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!
.insertAll(_handles!);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insertAll(_handles!);
}
/// {@template flutter.widgets.SelectionOverlay.hideHandles}
......@@ -1085,7 +1084,7 @@ class SelectionOverlay {
return;
}
_toolbar = OverlayEntry(builder: _buildToolbar);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor)!.insert(_toolbar!);
Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor).insert(_toolbar!);
}
bool _buildScheduled = false;
......
......@@ -226,7 +226,7 @@ void main() {
final OverlayEntry fakeBeforeOverlayEntry =
OverlayEntry(builder: (_) => fakeBefore);
Overlay.of(context)!.insert(fakeBeforeOverlayEntry);
Overlay.of(context).insert(fakeBeforeOverlayEntry);
magnifierController.show(
context: context,
builder: (_) => fakeMagnifier,
......
......@@ -694,7 +694,7 @@ void main() {
await tester.pump();
});
testWidgets('OverlayState.of() called without Overlay being exist', (WidgetTester tester) async {
testWidgets('OverlayState.of() throws when called if an Overlay does not exist', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
......@@ -712,7 +712,8 @@ void main() {
expect(error.diagnostics[2].level, DiagnosticLevel.hint);
expect(error.diagnostics[2].toStringDeep(), equalsIgnoringHashCodes(
'The most common way to add an Overlay to an application is to\n'
'include a MaterialApp or Navigator widget in the runApp() call.\n',
'include a MaterialApp, CupertinoApp or Navigator widget in the\n'
'runApp() call.\n'
));
expect(error.diagnostics[3], isA<DiagnosticsProperty<Widget>>());
expect(error.diagnostics[3].value, debugRequiredFor);
......@@ -723,12 +724,13 @@ void main() {
' Container widgets require an Overlay widget ancestor for correct\n'
' operation.\n'
' The most common way to add an Overlay to an application is to\n'
' include a MaterialApp or Navigator widget in the runApp() call.\n'
' include a MaterialApp, CupertinoApp or Navigator widget in the\n'
' runApp() call.\n'
' The specific widget that failed to find an overlay was:\n'
' Container\n'
' The context from which that widget was searching for an overlay\n'
' was:\n'
' Builder\n',
' Builder\n'
));
}
return Container();
......@@ -738,6 +740,47 @@ void main() {
);
});
testWidgets("OverlayState.maybeOf() works when an Overlay does and doesn't exist", (WidgetTester tester) async {
final GlobalKey overlayKey = GlobalKey();
OverlayState? foundState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
foundState = Overlay.maybeOf(context);
return Container();
},
),
],
),
),
);
expect(tester.takeException(), isNull);
expect(foundState, isNotNull);
foundState = null;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Builder(
builder: (BuildContext context) {
foundState = Overlay.maybeOf(context);
return const SizedBox();
},
),
),
);
expect(tester.takeException(), isNull);
expect(foundState, isNull);
});
testWidgets('OverlayEntry.opaque can be changed when OverlayEntry is not part of an Overlay (yet)', (WidgetTester tester) async {
final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>();
final Key root = UniqueKey();
......
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