123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- // Copyright 2014 The Flutter Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style license that can be
- // found in the LICENSE file.
- import 'dart:async';
- import 'package:flutter/foundation.dart';
- import 'package:flutter/gestures.dart';
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
- import 'package:flutter/services.dart';
- import 'package:flyinsonolite/infrastructure/scale.dart';
- /// Signature for when a tooltip is triggered.
- typedef TooltipTriggeredCallback = void Function();
- /// A Material Design tooltip.
- ///
- /// Tooltips provide text labels which help explain the function of a button or
- /// other user interface action. Wrap the button in a [CustomTooltip] widget and provide
- /// a message which will be shown when the widget is long pressed.
- ///
- /// Many widgets, such as [CustomIconButton], [FloatingActionButton], and
- /// [PopupMenuButton] have a `tooltip` property that, when non-null, causes the
- /// widget to include a [CustomTooltip] in its build.
- ///
- /// Tooltips improve the accessibility of visual widgets by proving a textual
- /// representation of the widget, which, for example, can be vocalized by a
- /// screen reader.
- ///
- /// {@youtube 560 315 https://www.youtube.com/watch?v=EeEfD5fI-5Q}
- ///
- /// {@tool dartpad}
- /// This example show a basic [CustomTooltip] which has a [Text] as child.
- /// [message] contains your label to be shown by the tooltip when
- /// the child that Tooltip wraps is hovered over on web or desktop. On mobile,
- /// the tooltip is shown when the widget is long pressed.
- ///
- /// ** See code in examples/api/lib/material/tooltip/tooltip.0.dart **
- /// {@end-tool}
- ///
- /// {@tool dartpad}
- /// This example covers most of the attributes available in Tooltip.
- /// `decoration` has been used to give a gradient and borderRadius to Tooltip.
- /// `height` has been used to set a specific height of the Tooltip.
- /// `preferBelow` is false, the tooltip will prefer showing above [CustomTooltip]'s child widget.
- /// However, it may show the tooltip below if there's not enough space
- /// above the widget.
- /// `textStyle` has been used to set the font size of the 'message'.
- /// `showDuration` accepts a Duration to continue showing the message after the long
- /// press has been released or the mouse pointer exits the child widget.
- /// `waitDuration` accepts a Duration for which a mouse pointer has to hover over the child
- /// widget before the tooltip is shown.
- ///
- /// ** See code in examples/api/lib/material/tooltip/tooltip.1.dart **
- /// {@end-tool}
- ///
- /// {@tool dartpad}
- /// This example shows a rich [CustomTooltip] that specifies the [richMessage]
- /// parameter instead of the [message] parameter (only one of these may be
- /// non-null. Any [InlineSpan] can be specified for the [richMessage] attribute,
- /// including [WidgetSpan].
- ///
- /// ** See code in examples/api/lib/material/tooltip/tooltip.2.dart **
- /// {@end-tool}
- ///
- /// {@tool dartpad}
- /// This example shows how [CustomTooltip] can be shown manually with [TooltipTriggerMode.manual]
- /// by calling the [CustomTooltipState.ensureTooltipVisible] function.
- ///
- /// ** See code in examples/api/lib/material/tooltip/tooltip.3.dart **
- /// {@end-tool}
- ///
- /// See also:
- ///
- /// * <https://material.io/design/components/tooltips.html>
- /// * [TooltipTheme] or [ThemeData.tooltipTheme]
- /// * [TooltipVisibility]
- class CustomTooltip extends StatefulWidget {
- /// Creates a tooltip.
- ///
- /// By default, tooltips should adhere to the
- /// [Material specification](https://material.io/design/components/tooltips.html#spec).
- /// If the optional constructor parameters are not defined, the values
- /// provided by [TooltipTheme.of] will be used if a [TooltipTheme] is present
- /// or specified in [ThemeData].
- ///
- /// All parameters that are defined in the constructor will
- /// override the default values _and_ the values in [TooltipTheme.of].
- ///
- /// Only one of [message] and [richMessage] may be non-null.
- const CustomTooltip({
- super.key,
- this.message,
- this.richMessage,
- this.height,
- this.padding,
- this.margin,
- this.verticalOffset,
- this.preferBelow,
- this.excludeFromSemantics,
- this.decoration,
- this.textStyle,
- this.textAlign,
- this.waitDuration,
- this.showDuration,
- this.triggerMode,
- this.enableFeedback,
- this.onTriggered,
- this.child,
- }) : assert((message == null) != (richMessage == null), 'Either `message` or `richMessage` must be specified'),
- assert(
- richMessage == null || textStyle == null,
- 'If `richMessage` is specified, `textStyle` will have no effect. '
- 'If you wish to provide a `textStyle` for a rich tooltip, add the '
- '`textStyle` directly to the `richMessage` InlineSpan.',
- );
- /// The text to display in the tooltip.
- ///
- /// Only one of [message] and [richMessage] may be non-null.
- final String? message;
- /// The rich text to display in the tooltip.
- ///
- /// Only one of [message] and [richMessage] may be non-null.
- final InlineSpan? richMessage;
- /// The height of the tooltip's [child].
- ///
- /// If the [child] is null, then this is the tooltip's intrinsic height.
- final double? height;
- /// The amount of space by which to inset the tooltip's [child].
- ///
- /// On mobile, defaults to 16.0 logical pixels horizontally and 4.0 vertically.
- /// On desktop, defaults to 8.0 logical pixels horizontally and 4.0 vertically.
- final EdgeInsetsGeometry? padding;
- /// The empty space that surrounds the tooltip.
- ///
- /// Defines the tooltip's outer [Container.margin]. By default, a
- /// long tooltip will span the width of its window. If long enough,
- /// a tooltip might also span the window's height. This property allows
- /// one to define how much space the tooltip must be inset from the edges
- /// of their display window.
- ///
- /// If this property is null, then [TooltipThemeData.margin] is used.
- /// If [TooltipThemeData.margin] is also null, the default margin is
- /// 0.0 logical pixels on all sides.
- final EdgeInsetsGeometry? margin;
- /// The vertical gap between the widget and the displayed tooltip.
- ///
- /// When [preferBelow] is set to true and tooltips have sufficient space to
- /// display themselves, this property defines how much vertical space
- /// tooltips will position themselves under their corresponding widgets.
- /// Otherwise, tooltips will position themselves above their corresponding
- /// widgets with the given offset.
- final double? verticalOffset;
- /// Whether the tooltip defaults to being displayed below the widget.
- ///
- /// Defaults to true. If there is insufficient space to display the tooltip in
- /// the preferred direction, the tooltip will be displayed in the opposite
- /// direction.
- final bool? preferBelow;
- /// Whether the tooltip's [message] or [richMessage] should be excluded from
- /// the semantics tree.
- ///
- /// Defaults to false. A tooltip will add a [Semantics] label that is set to
- /// [CustomTooltip.message] if non-null, or the plain text value of
- /// [CustomTooltip.richMessage] otherwise. Set this property to true if the app is
- /// going to provide its own custom semantics label.
- final bool? excludeFromSemantics;
- /// The widget below this widget in the tree.
- ///
- /// {@macro flutter.widgets.ProxyWidget.child}
- final Widget? child;
- /// Specifies the tooltip's shape and background color.
- ///
- /// The tooltip shape defaults to a rounded rectangle with a border radius of
- /// 4.0. Tooltips will also default to an opacity of 90% and with the color
- /// [Colors.grey]\[700\] if [ThemeData.brightness] is [Brightness.dark], and
- /// [Colors.white] if it is [Brightness.light].
- final Decoration? decoration;
- /// The style to use for the message of the tooltip.
- ///
- /// If null, the message's [TextStyle] will be determined based on
- /// [ThemeData]. If [ThemeData.brightness] is set to [Brightness.dark],
- /// [TextTheme.bodyMedium] of [ThemeData.textTheme] will be used with
- /// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
- /// [Brightness.light], [TextTheme.bodyMedium] of [ThemeData.textTheme] will be
- /// used with [Colors.black].
- final TextStyle? textStyle;
- /// How the message of the tooltip is aligned horizontally.
- ///
- /// If this property is null, then [TooltipThemeData.textAlign] is used.
- /// If [TooltipThemeData.textAlign] is also null, the default value is
- /// [TextAlign.start].
- final TextAlign? textAlign;
- /// The length of time that a pointer must hover over a tooltip's widget
- /// before the tooltip will be shown.
- ///
- /// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
- final Duration? waitDuration;
- /// The length of time that the tooltip will be shown after a long press is
- /// released (if triggerMode is [TooltipTriggerMode.longPress]) or a tap is
- /// released (if triggerMode is [TooltipTriggerMode.tap]) or mouse pointer
- /// exits the widget.
- ///
- /// Defaults to 1.5 seconds for long press and tap released or 0.1 seconds
- /// for mouse pointer exits the widget.
- final Duration? showDuration;
- /// The [TooltipTriggerMode] that will show the tooltip.
- ///
- /// If this property is null, then [TooltipThemeData.triggerMode] is used.
- /// If [TooltipThemeData.triggerMode] is also null, the default mode is
- /// [TooltipTriggerMode.longPress].
- final TooltipTriggerMode? triggerMode;
- /// Whether the tooltip should provide acoustic and/or haptic feedback.
- ///
- /// For example, on Android a tap will produce a clicking sound and a
- /// long-press will produce a short vibration, when feedback is enabled.
- ///
- /// When null, the default value is true.
- ///
- /// See also:
- ///
- /// * [Feedback], for providing platform-specific feedback to certain actions.
- final bool? enableFeedback;
- /// Called when the Tooltip is triggered.
- ///
- /// The tooltip is triggered after a tap when [triggerMode] is [TooltipTriggerMode.tap]
- /// or after a long press when [triggerMode] is [TooltipTriggerMode.longPress].
- final TooltipTriggeredCallback? onTriggered;
- static final List<CustomTooltipState> _openedTooltips = <CustomTooltipState>[];
- // Causes any current tooltips to be concealed. Only called for mouse hover enter
- // detections. Won't conceal the supplied tooltip.
- static void _concealOtherTooltips(CustomTooltipState current) {
- if (_openedTooltips.isNotEmpty) {
- // Avoid concurrent modification.
- final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
- for (final CustomTooltipState 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.
- ///
- /// This method returns true if it successfully dismisses the tooltips. It
- /// returns false if there is no tooltip shown on the screen.
- static bool dismissAllToolTips() {
- if (_openedTooltips.isNotEmpty) {
- // Avoid concurrent modification.
- final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
- for (final CustomTooltipState state in openedTooltips) {
- state._dismissTooltip(immediately: true);
- }
- return true;
- }
- return false;
- }
- @override
- State<CustomTooltip> createState() => CustomTooltipState();
- @override
- void debugFillProperties(DiagnosticPropertiesBuilder properties) {
- super.debugFillProperties(properties);
- properties.add(StringProperty(
- 'message',
- message,
- showName: message == null,
- defaultValue: message == null ? null : kNoDefaultValue,
- ));
- properties.add(StringProperty(
- 'richMessage',
- richMessage?.toPlainText(),
- showName: richMessage == null,
- defaultValue: richMessage == null ? null : kNoDefaultValue,
- ));
- properties.add(DoubleProperty('height', height, defaultValue: null));
- properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
- properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
- properties.add(DoubleProperty('vertical offset', verticalOffset, defaultValue: null));
- properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true));
- properties.add(FlagProperty('semantics', value: excludeFromSemantics, ifTrue: 'excluded', showName: true));
- properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null));
- properties.add(DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null));
- properties.add(DiagnosticsProperty<TooltipTriggerMode>('triggerMode', triggerMode, defaultValue: null));
- properties.add(FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true));
- properties.add(DiagnosticsProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
- }
- }
- /// Contains the state for a [CustomTooltip].
- ///
- /// This class can be used to programmatically show the Tooltip, see the
- /// [ensureTooltipVisible] method.
- class CustomTooltipState extends State<CustomTooltip> with SingleTickerProviderStateMixin {
- final double _defaultVerticalOffset = 24.s;
- static const bool _defaultPreferBelow = true;
- static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
- static const Duration _fadeInDuration = Duration(milliseconds: 150);
- static const Duration _fadeOutDuration = Duration(milliseconds: 75);
- static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
- static const Duration _defaultHoverShowDuration = Duration(milliseconds: 100);
- static const Duration _defaultWaitDuration = Duration.zero;
- static const bool _defaultExcludeFromSemantics = false;
- static const TooltipTriggerMode _defaultTriggerMode = TooltipTriggerMode.longPress;
- static const bool _defaultEnableFeedback = true;
- static const TextAlign _defaultTextAlign = TextAlign.start;
- late double _height;
- late EdgeInsetsGeometry _padding;
- late EdgeInsetsGeometry _margin;
- late Decoration _decoration;
- late TextStyle _textStyle;
- late TextAlign _textAlign;
- late double _verticalOffset;
- late bool _preferBelow;
- late bool _excludeFromSemantics;
- late AnimationController _controller;
- OverlayEntry? _entry;
- Timer? _dismissTimer;
- Timer? _showTimer;
- late Duration _showDuration;
- late Duration _hoverShowDuration;
- late Duration _waitDuration;
- late bool _mouseIsConnected;
- bool _pressActivated = false;
- late TooltipTriggerMode _triggerMode;
- late bool _enableFeedback;
- late bool _isConcealed;
- late bool _forceRemoval;
- late bool _visible;
- /// The plain text message for this tooltip.
- ///
- /// This value will either come from [widget.message] or [widget.richMessage].
- String get _tooltipMessage => widget.message ?? widget.richMessage!.toPlainText();
- @override
- void initState() {
- super.initState();
- _isConcealed = false;
- _forceRemoval = false;
- _mouseIsConnected = RendererBinding.instance.mouseTracker.mouseIsConnected;
- _controller = AnimationController(
- duration: _fadeInDuration,
- reverseDuration: _fadeOutDuration,
- vsync: this,
- )
- ..addStatusListener(_handleStatusChanged);
- // Listen to see when a mouse is added.
- RendererBinding.instance.mouseTracker.addListener(_handleMouseTrackerChange);
- // Listen to global pointer events so that we can hide a tooltip immediately
- // if some other control is clicked on.
- GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
- }
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- _visible = TooltipVisibility.of(context);
- }
- // https://material.io/components/tooltips#specs
- double _getDefaultTooltipHeight() {
- final ThemeData theme = Theme.of(context);
- switch (theme.platform) {
- case TargetPlatform.macOS:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- return 24.s;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.iOS:
- return 32.s;
- }
- }
- EdgeInsets _getDefaultPadding() {
- final ThemeData theme = Theme.of(context);
- switch (theme.platform) {
- case TargetPlatform.macOS:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- return EdgeInsets.symmetric(horizontal: 8.s, vertical: 4.s);
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.iOS:
- return EdgeInsets.symmetric(horizontal: 16.s, vertical: 4.s);
- }
- }
- double _getDefaultFontSize() {
- final ThemeData theme = Theme.of(context);
- switch (theme.platform) {
- case TargetPlatform.macOS:
- case TargetPlatform.linux:
- case TargetPlatform.windows:
- return 12.s;
- case TargetPlatform.android:
- case TargetPlatform.fuchsia:
- case TargetPlatform.iOS:
- return 14.s;
- }
- }
- // Forces a rebuild if a mouse has been added or removed.
- void _handleMouseTrackerChange() {
- if (!mounted) {
- return;
- }
- final bool mouseIsConnected = RendererBinding.instance.mouseTracker.mouseIsConnected;
- if (mouseIsConnected != _mouseIsConnected) {
- setState(() {
- _mouseIsConnected = mouseIsConnected;
- });
- }
- }
- void _handleStatusChanged(AnimationStatus status) {
- // If this tip is concealed, don't remove it, even if it is dismissed, so that we can
- // reveal it later, unless it has explicitly been hidden with _dismissTooltip.
- if (status == AnimationStatus.dismissed && (_forceRemoval || !_isConcealed)) {
- _removeEntry();
- }
- }
- void _dismissTooltip({ bool immediately = false }) {
- _showTimer?.cancel();
- _showTimer = null;
- if (immediately) {
- _removeEntry();
- return;
- }
- // So it will be removed when it's done reversing, regardless of whether it is
- // still concealed or not.
- _forceRemoval = true;
- if (_pressActivated) {
- _dismissTimer ??= Timer(_showDuration, _controller.reverse);
- } else {
- _dismissTimer ??= Timer(_hoverShowDuration, _controller.reverse);
- }
- _pressActivated = false;
- }
- void _showTooltip({ bool immediately = false }) {
- _dismissTimer?.cancel();
- _dismissTimer = null;
- if (immediately) {
- ensureTooltipVisible();
- return;
- }
- _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(_tooltipMessage);
- _controller.forward();
- }
- /// Shows the tooltip if it is not already visible.
- ///
- /// Returns `false` when the tooltip shouldn't be shown or when the tooltip
- /// was already visible.
- bool ensureTooltipVisible() {
- if (!_visible || !mounted) {
- return false;
- }
- _showTimer?.cancel();
- _showTimer = null;
- _forceRemoval = false;
- if (_isConcealed) {
- if (_mouseIsConnected) {
- CustomTooltip._concealOtherTooltips(this);
- }
- _revealTooltip();
- return true;
- }
- if (_entry != null) {
- // Stop trying to hide, if we were.
- _dismissTimer?.cancel();
- _dismissTimer = null;
- _controller.forward();
- return false; // Already visible.
- }
- _createNewEntry();
- _controller.forward();
- return true;
- }
- static final Set<CustomTooltipState> _mouseIn = <CustomTooltipState>{};
- void _handleMouseEnter() {
- if (mounted) {
- _showTooltip();
- }
- }
- void _handleMouseExit({bool immediately = false}) {
- if (mounted) {
- // If the tip is currently covered, we can just remove it without waiting.
- _dismissTooltip(immediately: _isConcealed || immediately);
- }
- }
- void _createNewEntry() {
- final OverlayState overlayState = Overlay.of(
- context,
- debugRequiredFor: widget,
- );
- final RenderBox box = context.findRenderObject()! as RenderBox;
- final Offset target = box.localToGlobal(
- box.size.center(Offset.zero),
- ancestor: overlayState.context.findRenderObject(),
- );
- // We create this widget outside of the overlay entry's builder to prevent
- // updated values from happening to leak into the overlay when the overlay
- // rebuilds.
- final Widget overlay = Directionality(
- textDirection: Directionality.of(context),
- child: _TooltipOverlay(
- richMessage: widget.richMessage ?? TextSpan(text: widget.message),
- height: _height,
- padding: _padding,
- margin: _margin,
- onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null,
- onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null,
- decoration: _decoration,
- textStyle: _textStyle,
- textAlign: _textAlign,
- animation: CurvedAnimation(
- parent: _controller,
- curve: Curves.fastOutSlowIn,
- ),
- target: target,
- verticalOffset: _verticalOffset,
- preferBelow: _preferBelow,
- ),
- );
- _entry = OverlayEntry(builder: (BuildContext context) => overlay);
- _isConcealed = false;
- overlayState.insert(_entry!);
- SemanticsService.tooltip(_tooltipMessage);
- if (_mouseIsConnected) {
- // 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.
- CustomTooltip._concealOtherTooltips(this);
- }
- assert(!CustomTooltip._openedTooltips.contains(this));
- CustomTooltip._openedTooltips.add(this);
- }
- void _removeEntry() {
- CustomTooltip._openedTooltips.remove(this);
- _mouseIn.remove(this);
- _dismissTimer?.cancel();
- _dismissTimer = null;
- _showTimer?.cancel();
- _showTimer = null;
- if (!_isConcealed) {
- _entry?.remove();
- }
- _isConcealed = false;
- _entry = null;
- if (_mouseIsConnected) {
- CustomTooltip._revealLastTooltip();
- }
- }
- void _handlePointerEvent(PointerEvent event) {
- if (_entry == null) {
- return;
- }
- if (event is PointerUpEvent || event is PointerCancelEvent) {
- _handleMouseExit();
- } else if (event is PointerDownEvent) {
- _handleMouseExit(immediately: true);
- }
- }
- @override
- void deactivate() {
- if (_entry != null) {
- _dismissTooltip(immediately: true);
- }
- _showTimer?.cancel();
- super.deactivate();
- }
- @override
- void dispose() {
- GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
- RendererBinding.instance.mouseTracker.removeListener(_handleMouseTrackerChange);
- _removeEntry();
- _controller.dispose();
- super.dispose();
- }
- void _handlePress() {
- _pressActivated = true;
- final bool tooltipCreated = ensureTooltipVisible();
- if (tooltipCreated && _enableFeedback) {
- if (_triggerMode == TooltipTriggerMode.longPress) {
- Feedback.forLongPress(context);
- } else {
- Feedback.forTap(context);
- }
- }
- widget.onTriggered?.call();
- }
- void _handleTap() {
- _handlePress();
- // When triggerMode is not [TooltipTriggerMode.tap] the tooltip is dismissed
- // by _handlePointerEvent, which listens to the global pointer events.
- // When triggerMode is [TooltipTriggerMode.tap] and the Tooltip GestureDetector
- // competes with other GestureDetectors, the disambiguation process will complete
- // after the global pointer event is received. As we can't rely on the global
- // pointer events to dismiss the Tooltip, we have to call _handleMouseExit
- // to dismiss the tooltip after _showDuration expired.
- _handleMouseExit();
- }
- @override
- Widget build(BuildContext context) {
- // If message is empty then no need to create a tooltip overlay to show
- // the empty black container so just return the wrapped child as is or
- // empty container if child is not specified.
- if (_tooltipMessage.isEmpty) {
- return widget.child ?? const SizedBox.shrink();
- }
- assert(debugCheckHasOverlay(context));
- final ThemeData theme = Theme.of(context);
- final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
- final TextStyle defaultTextStyle;
- final BoxDecoration defaultDecoration;
- if (theme.brightness == Brightness.dark) {
- defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
- color: Colors.black,
- fontSize: _getDefaultFontSize(),
- );
- defaultDecoration = BoxDecoration(
- color: Colors.white.withOpacity(0.9),
- borderRadius: const BorderRadius.all(Radius.circular(4)),
- );
- } else {
- defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
- color: Colors.white,
- fontSize: _getDefaultFontSize(),
- );
- defaultDecoration = BoxDecoration(
- color: Colors.grey[700]!.withOpacity(0.9),
- borderRadius: const BorderRadius.all(Radius.circular(4)),
- );
- }
- _height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
- _padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
- _margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
- _verticalOffset = widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
- _preferBelow = widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow;
- _excludeFromSemantics = widget.excludeFromSemantics ?? tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
- _decoration = widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
- _textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
- _textAlign = widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign;
- _waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
- _showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;
- _hoverShowDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultHoverShowDuration;
- _triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
- _enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
- Widget result = Semantics(
- tooltip: _excludeFromSemantics
- ? null
- : _tooltipMessage,
- child: widget.child,
- );
- // Only check for gestures if tooltip should be visible.
- if (_visible) {
- result = GestureDetector(
- behavior: HitTestBehavior.opaque,
- onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
- onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handleTap : null,
- excludeFromSemantics: true,
- child: result,
- );
- // Only check for hovering if there is a mouse connected.
- if (_mouseIsConnected) {
- result = MouseRegion(
- onEnter: (_) => _handleMouseEnter(),
- onExit: (_) => _handleMouseExit(),
- child: result,
- );
- }
- }
- return result;
- }
- }
- /// A delegate for computing the layout of a tooltip to be displayed above or
- /// below a target specified in the global coordinate system.
- class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
- /// Creates a delegate for computing the layout of a tooltip.
- ///
- /// The arguments must not be null.
- _TooltipPositionDelegate({
- required this.target,
- required this.verticalOffset,
- required this.preferBelow,
- }) : assert(target != null),
- assert(verticalOffset != null),
- assert(preferBelow != null);
- /// The offset of the target the tooltip is positioned near in the global
- /// coordinate system.
- final Offset target;
- /// The amount of vertical distance between the target and the displayed
- /// tooltip.
- final double verticalOffset;
- /// Whether the tooltip is displayed below its widget by default.
- ///
- /// If there is insufficient space to display the tooltip in the preferred
- /// direction, the tooltip will be displayed in the opposite direction.
- final bool preferBelow;
- @override
- BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
- @override
- Offset getPositionForChild(Size size, Size childSize) {
- return positionDependentBox(
- size: size,
- childSize: childSize,
- target: target,
- verticalOffset: verticalOffset,
- preferBelow: preferBelow,
- );
- }
- @override
- bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
- return target != oldDelegate.target
- || verticalOffset != oldDelegate.verticalOffset
- || preferBelow != oldDelegate.preferBelow;
- }
- }
- class _TooltipOverlay extends StatelessWidget {
- const _TooltipOverlay({
- required this.height,
- required this.richMessage,
- this.padding,
- this.margin,
- this.decoration,
- this.textStyle,
- this.textAlign,
- required this.animation,
- required this.target,
- required this.verticalOffset,
- required this.preferBelow,
- this.onEnter,
- this.onExit,
- });
- final InlineSpan richMessage;
- final double height;
- final EdgeInsetsGeometry? padding;
- final EdgeInsetsGeometry? margin;
- final Decoration? decoration;
- final TextStyle? textStyle;
- final TextAlign? textAlign;
- final Animation<double> animation;
- final Offset target;
- final double verticalOffset;
- final bool preferBelow;
- final PointerEnterEventListener? onEnter;
- final PointerExitEventListener? onExit;
- @override
- Widget build(BuildContext context) {
- Widget result = IgnorePointer(
- child: FadeTransition(
- opacity: animation,
- child: ConstrainedBox(
- constraints: BoxConstraints(minHeight: height),
- child: DefaultTextStyle(
- style: Theme.of(context).textTheme.bodyMedium!,
- child: Container(
- decoration: decoration,
- padding: padding,
- margin: margin,
- child: Center(
- widthFactor: 1.0,
- heightFactor: 1.0,
- child: Text.rich(
- richMessage,
- style: textStyle,
- textAlign: textAlign,
- ),
- ),
- ),
- ),
- ),
- )
- );
- if (onEnter != null || onExit != null) {
- result = MouseRegion(
- onEnter: onEnter,
- onExit: onExit,
- child: result,
- );
- }
- return Positioned.fill(
- bottom: MediaQuery.maybeOf(context)?.viewInsets.bottom ?? 0.0,
- child: CustomSingleChildLayout(
- delegate: _TooltipPositionDelegate(
- target: target,
- verticalOffset: verticalOffset,
- preferBelow: preferBelow,
- ),
- child: result,
- ),
- );
- }
- }
|