Unverified Commit ab51a026 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Revert "Fix tooltip so only one shows at a time when hovering (#90457)" (#90909)

This reverts commit 885b2f56 to green up the build.

Submitting on red to fix the build.
parent 777463c2
...@@ -194,41 +194,18 @@ class Tooltip extends StatefulWidget { ...@@ -194,41 +194,18 @@ class Tooltip extends StatefulWidget {
/// * [Feedback], for providing platform-specific feedback to certain actions. /// * [Feedback], for providing platform-specific feedback to certain actions.
final bool? enableFeedback; final bool? enableFeedback;
static final List<_TooltipState> _openedTooltips = <_TooltipState>[]; static final Set<_TooltipState> _openedToolTips = <_TooltipState>{};
// Causes any current tooltips to be concealed. Only called for mouse hover enter
// detections. Won't conceal the supplied tooltip.
static void _concealOtherTooltips(_TooltipState current) {
if (_openedTooltips.isNotEmpty) {
// Avoid concurrent modification.
final List<_TooltipState> openedTooltips = _openedTooltips.toList();
for (final _TooltipState state in openedTooltips) {
if (state == current) {
continue;
}
state._concealTooltip();
}
}
}
// Causes the most recently concealed tooltip to be revealed. Only called for mouse
// hover exit detections.
static void _revealLastTooltip() {
if (_openedTooltips.isNotEmpty) {
_openedTooltips.last._revealTooltip();
}
}
/// Dismiss all of the tooltips that are currently shown on the screen. /// Dismiss all of the tooltips that are currently shown on the screen.
/// ///
/// This method returns true if it successfully dismisses the tooltips. It /// This method returns true if it successfully dismisses the tooltips. It
/// returns false if there is no tooltip shown on the screen. /// returns false if there is no tooltip shown on the screen.
static bool dismissAllToolTips() { static bool dismissAllToolTips() {
if (_openedTooltips.isNotEmpty) { if (_openedToolTips.isNotEmpty) {
// Avoid concurrent modification. // Avoid concurrent modification.
final List<_TooltipState> openedTooltips = _openedTooltips.toList(); final List<_TooltipState> openedToolTips = List<_TooltipState>.from(_openedToolTips);
for (final _TooltipState state in openedTooltips) { for (final _TooltipState state in openedToolTips) {
state._dismissTooltip(immediately: true); state._hideTooltip(immediately: true);
} }
return true; return true;
} }
...@@ -278,7 +255,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -278,7 +255,7 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
late bool excludeFromSemantics; late bool excludeFromSemantics;
late AnimationController _controller; late AnimationController _controller;
OverlayEntry? _entry; OverlayEntry? _entry;
Timer? _dismissTimer; Timer? _hideTimer;
Timer? _showTimer; Timer? _showTimer;
late Duration showDuration; late Duration showDuration;
late Duration hoverShowDuration; late Duration hoverShowDuration;
...@@ -287,14 +264,10 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -287,14 +264,10 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
bool _pressActivated = false; bool _pressActivated = false;
late TooltipTriggerMode triggerMode; late TooltipTriggerMode triggerMode;
late bool enableFeedback; late bool enableFeedback;
late bool _isConcealed;
late bool _forceRemoval;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isConcealed = false;
_forceRemoval = false;
_mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected; _mouseIsConnected = RendererBinding.instance!.mouseTracker.mouseIsConnected;
_controller = AnimationController( _controller = AnimationController(
duration: _fadeInDuration, duration: _fadeInDuration,
...@@ -360,34 +333,29 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -360,34 +333,29 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
} }
void _handleStatusChanged(AnimationStatus status) { void _handleStatusChanged(AnimationStatus status) {
// If this tip is concealed, don't remove it, even if it is dismissed, so that we can if (status == AnimationStatus.dismissed) {
// reveal it later, unless it has explicitly been hidden with _dismissTooltip. _hideTooltip(immediately: true);
if (status == AnimationStatus.dismissed && (_forceRemoval || !_isConcealed)) {
_removeEntry();
} }
} }
void _dismissTooltip({ bool immediately = false }) { void _hideTooltip({ bool immediately = false }) {
_showTimer?.cancel(); _showTimer?.cancel();
_showTimer = null; _showTimer = null;
if (immediately) { if (immediately) {
_removeEntry(); _removeEntry();
return; return;
} }
// So it will be removed when it's done reversing, regardless of whether it is
// still concealed or not.
_forceRemoval = true;
if (_pressActivated) { if (_pressActivated) {
_dismissTimer ??= Timer(showDuration, _controller.reverse); _hideTimer ??= Timer(showDuration, _controller.reverse);
} else { } else {
_dismissTimer ??= Timer(hoverShowDuration, _controller.reverse); _hideTimer ??= Timer(hoverShowDuration, _controller.reverse);
} }
_pressActivated = false; _pressActivated = false;
} }
void _showTooltip({ bool immediately = false }) { void _showTooltip({ bool immediately = false }) {
_dismissTimer?.cancel(); _hideTimer?.cancel();
_dismissTimer = null; _hideTimer = null;
if (immediately) { if (immediately) {
ensureTooltipVisible(); ensureTooltipVisible();
return; return;
...@@ -395,61 +363,17 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -395,61 +363,17 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
_showTimer ??= Timer(waitDuration, ensureTooltipVisible); _showTimer ??= Timer(waitDuration, ensureTooltipVisible);
} }
void _concealTooltip() {
if (_isConcealed || _forceRemoval) {
// Already concealed, or it's being removed.
return;
}
_isConcealed = true;
_dismissTimer?.cancel();
_dismissTimer = null;
_showTimer?.cancel();
_showTimer = null;
if (_entry!= null) {
_entry!.remove();
}
_controller.reverse();
}
void _revealTooltip() {
if (!_isConcealed) {
// Already uncovered.
return;
}
_isConcealed = false;
_dismissTimer?.cancel();
_dismissTimer = null;
_showTimer?.cancel();
_showTimer = null;
if (!_entry!.mounted) {
final OverlayState overlayState = Overlay.of(
context,
debugRequiredFor: widget,
)!;
overlayState.insert(_entry!);
}
SemanticsService.tooltip(widget.message);
_controller.forward();
}
/// Shows the tooltip if it is not already visible. /// Shows the tooltip if it is not already visible.
/// ///
/// Returns `false` when the tooltip was already visible. /// Returns `false` when the tooltip was already visible or if the context has
/// become null.
bool ensureTooltipVisible() { bool ensureTooltipVisible() {
_showTimer?.cancel(); _showTimer?.cancel();
_showTimer = null; _showTimer = null;
_forceRemoval = false;
if (_isConcealed) {
if (_mouseIsConnected) {
Tooltip._concealOtherTooltips(this);
}
_revealTooltip();
return true;
}
if (_entry != null) { if (_entry != null) {
// Stop trying to hide, if we were. // Stop trying to hide, if we were.
_dismissTimer?.cancel(); _hideTimer?.cancel();
_dismissTimer = null; _hideTimer = null;
_controller.forward(); _controller.forward();
return false; // Already visible. return false; // Already visible.
} }
...@@ -458,17 +382,6 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -458,17 +382,6 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
return true; return true;
} }
static final Set<_TooltipState> _mouseIn = <_TooltipState>{};
void _handleMouseEnter() {
_showTooltip();
}
void _handleMouseExit({bool immediately = false}) {
// If the tip is currently covered, we can just remove it without waiting.
_dismissTooltip(immediately: _isConcealed || immediately);
}
void _createNewEntry() { void _createNewEntry() {
final OverlayState overlayState = Overlay.of( final OverlayState overlayState = Overlay.of(
context, context,
...@@ -491,8 +404,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -491,8 +404,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
height: height, height: height,
padding: padding, padding: padding,
margin: margin, margin: margin,
onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null, onEnter: _mouseIsConnected ? (PointerEnterEvent event) => _showTooltip() : null,
onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null, onExit: _mouseIsConnected ? (PointerExitEvent event) => _hideTooltip() : null,
decoration: decoration, decoration: decoration,
textStyle: textStyle, textStyle: textStyle,
animation: CurvedAnimation( animation: CurvedAnimation(
...@@ -505,34 +418,19 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -505,34 +418,19 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
), ),
); );
_entry = OverlayEntry(builder: (BuildContext context) => overlay); _entry = OverlayEntry(builder: (BuildContext context) => overlay);
_isConcealed = false;
overlayState.insert(_entry!); overlayState.insert(_entry!);
SemanticsService.tooltip(widget.message); SemanticsService.tooltip(widget.message);
if (_mouseIsConnected) { Tooltip._openedToolTips.add(this);
// Hovered tooltips shouldn't show more than one at once. For example, a chip with
// a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
// at the same time.
Tooltip._concealOtherTooltips(this);
}
assert(!Tooltip._openedTooltips.contains(this));
Tooltip._openedTooltips.add(this);
} }
void _removeEntry() { void _removeEntry() {
Tooltip._openedTooltips.remove(this); Tooltip._openedToolTips.remove(this);
_mouseIn.remove(this); _hideTimer?.cancel();
_dismissTimer?.cancel(); _hideTimer = null;
_dismissTimer = null;
_showTimer?.cancel(); _showTimer?.cancel();
_showTimer = null; _showTimer = null;
if (!_isConcealed) {
_entry?.remove(); _entry?.remove();
}
_isConcealed = false;
_entry = null; _entry = null;
if (_mouseIsConnected) {
Tooltip._revealLastTooltip();
}
} }
void _handlePointerEvent(PointerEvent event) { void _handlePointerEvent(PointerEvent event) {
...@@ -540,16 +438,16 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -540,16 +438,16 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
return; return;
} }
if (event is PointerUpEvent || event is PointerCancelEvent) { if (event is PointerUpEvent || event is PointerCancelEvent) {
_handleMouseExit(); _hideTooltip();
} else if (event is PointerDownEvent) { } else if (event is PointerDownEvent) {
_handleMouseExit(immediately: true); _hideTooltip(immediately: true);
} }
} }
@override @override
void deactivate() { void deactivate() {
if (_entry != null) { if (_entry != null) {
_dismissTooltip(immediately: true); _hideTooltip(immediately: true);
} }
_showTimer?.cancel(); _showTimer?.cancel();
super.deactivate(); super.deactivate();
...@@ -637,8 +535,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin { ...@@ -637,8 +535,8 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
// Only check for hovering if there is a mouse connected. // Only check for hovering if there is a mouse connected.
if (_mouseIsConnected) { if (_mouseIsConnected) {
result = MouseRegion( result = MouseRegion(
onEnter: (_) => _handleMouseEnter(), onEnter: (PointerEnterEvent event) => _showTooltip(),
onExit: (_) => _handleMouseExit(), onExit: (PointerExitEvent event) => _hideTooltip(),
child: result, child: result,
); );
} }
......
...@@ -919,7 +919,8 @@ void main() { ...@@ -919,7 +919,8 @@ void main() {
const Duration waitDuration = Duration.zero; const Duration waitDuration = Duration.zero;
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(() async { addTearDown(() async {
gesture?.removePointer(); if (gesture != null)
return gesture.removePointer();
}); });
await gesture.addPointer(); await gesture.addPointer();
await gesture.moveTo(const Offset(1.0, 1.0)); await gesture.moveTo(const Offset(1.0, 1.0));
...@@ -969,70 +970,6 @@ void main() { ...@@ -969,70 +970,6 @@ void main() {
expect(find.text(tooltipText), findsNothing); expect(find.text(tooltipText), findsNothing);
}); });
testWidgets('Tooltip should not show more than one tooltip when hovered', (WidgetTester tester) async {
const Duration waitDuration = Duration(milliseconds: 500);
final UniqueKey innerKey = UniqueKey();
final UniqueKey outerKey = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Center(
child: Tooltip(
message: 'Outer',
child: Container(
key: outerKey,
width: 100,
height: 100,
alignment: Alignment.centerRight,
child: Tooltip(
message: 'Inner',
child: SizedBox(
key: innerKey,
width: 25,
height: 100,
),
),
),
),
),
),
);
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(() async { gesture?.removePointer(); });
// Both the inner and outer containers have tooltips associated with them, but only
// the currently hovered one should appear, even though the pointer is inside both.
final Finder outer = find.byKey(outerKey);
final Finder inner = find.byKey(innerKey);
await gesture.moveTo(Offset.zero);
await tester.pump();
await gesture.moveTo(tester.getCenter(outer));
await tester.pump();
await gesture.moveTo(tester.getCenter(inner));
await tester.pump();
// Wait for it to appear.
await tester.pump(waitDuration);
expect(find.text('Outer'), findsNothing);
expect(find.text('Inner'), findsOneWidget);
await gesture.moveTo(tester.getCenter(outer));
await tester.pump();
// Wait for it to switch.
await tester.pump(waitDuration);
expect(find.text('Outer'), findsOneWidget);
expect(find.text('Inner'), findsNothing);
await gesture.moveTo(Offset.zero);
// Wait for all tooltips to disappear.
await tester.pumpAndSettle();
await gesture.removePointer();
gesture = null;
expect(find.text('Outer'), findsNothing);
expect(find.text('Inner'), findsNothing);
});
testWidgets('Tooltip can be dismissed by escape key', (WidgetTester tester) async { testWidgets('Tooltip can be dismissed by escape key', (WidgetTester tester) async {
const Duration waitDuration = Duration.zero; const Duration waitDuration = Duration.zero;
TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); TestGesture? gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
......
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