customtooltip.dart 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  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. import 'dart:async';
  5. import 'package:flutter/foundation.dart';
  6. import 'package:flutter/gestures.dart';
  7. import 'package:flutter/material.dart';
  8. import 'package:flutter/rendering.dart';
  9. import 'package:flutter/services.dart';
  10. import 'package:flyinsonolite/infrastructure/scale.dart';
  11. /// Signature for when a tooltip is triggered.
  12. typedef TooltipTriggeredCallback = void Function();
  13. /// A Material Design tooltip.
  14. ///
  15. /// Tooltips provide text labels which help explain the function of a button or
  16. /// other user interface action. Wrap the button in a [CustomTooltip] widget and provide
  17. /// a message which will be shown when the widget is long pressed.
  18. ///
  19. /// Many widgets, such as [CustomIconButton], [FloatingActionButton], and
  20. /// [PopupMenuButton] have a `tooltip` property that, when non-null, causes the
  21. /// widget to include a [CustomTooltip] in its build.
  22. ///
  23. /// Tooltips improve the accessibility of visual widgets by proving a textual
  24. /// representation of the widget, which, for example, can be vocalized by a
  25. /// screen reader.
  26. ///
  27. /// {@youtube 560 315 https://www.youtube.com/watch?v=EeEfD5fI-5Q}
  28. ///
  29. /// {@tool dartpad}
  30. /// This example show a basic [CustomTooltip] which has a [Text] as child.
  31. /// [message] contains your label to be shown by the tooltip when
  32. /// the child that Tooltip wraps is hovered over on web or desktop. On mobile,
  33. /// the tooltip is shown when the widget is long pressed.
  34. ///
  35. /// ** See code in examples/api/lib/material/tooltip/tooltip.0.dart **
  36. /// {@end-tool}
  37. ///
  38. /// {@tool dartpad}
  39. /// This example covers most of the attributes available in Tooltip.
  40. /// `decoration` has been used to give a gradient and borderRadius to Tooltip.
  41. /// `height` has been used to set a specific height of the Tooltip.
  42. /// `preferBelow` is false, the tooltip will prefer showing above [CustomTooltip]'s child widget.
  43. /// However, it may show the tooltip below if there's not enough space
  44. /// above the widget.
  45. /// `textStyle` has been used to set the font size of the 'message'.
  46. /// `showDuration` accepts a Duration to continue showing the message after the long
  47. /// press has been released or the mouse pointer exits the child widget.
  48. /// `waitDuration` accepts a Duration for which a mouse pointer has to hover over the child
  49. /// widget before the tooltip is shown.
  50. ///
  51. /// ** See code in examples/api/lib/material/tooltip/tooltip.1.dart **
  52. /// {@end-tool}
  53. ///
  54. /// {@tool dartpad}
  55. /// This example shows a rich [CustomTooltip] that specifies the [richMessage]
  56. /// parameter instead of the [message] parameter (only one of these may be
  57. /// non-null. Any [InlineSpan] can be specified for the [richMessage] attribute,
  58. /// including [WidgetSpan].
  59. ///
  60. /// ** See code in examples/api/lib/material/tooltip/tooltip.2.dart **
  61. /// {@end-tool}
  62. ///
  63. /// {@tool dartpad}
  64. /// This example shows how [CustomTooltip] can be shown manually with [TooltipTriggerMode.manual]
  65. /// by calling the [CustomTooltipState.ensureTooltipVisible] function.
  66. ///
  67. /// ** See code in examples/api/lib/material/tooltip/tooltip.3.dart **
  68. /// {@end-tool}
  69. ///
  70. /// See also:
  71. ///
  72. /// * <https://material.io/design/components/tooltips.html>
  73. /// * [TooltipTheme] or [ThemeData.tooltipTheme]
  74. /// * [TooltipVisibility]
  75. class CustomTooltip extends StatefulWidget {
  76. /// Creates a tooltip.
  77. ///
  78. /// By default, tooltips should adhere to the
  79. /// [Material specification](https://material.io/design/components/tooltips.html#spec).
  80. /// If the optional constructor parameters are not defined, the values
  81. /// provided by [TooltipTheme.of] will be used if a [TooltipTheme] is present
  82. /// or specified in [ThemeData].
  83. ///
  84. /// All parameters that are defined in the constructor will
  85. /// override the default values _and_ the values in [TooltipTheme.of].
  86. ///
  87. /// Only one of [message] and [richMessage] may be non-null.
  88. const CustomTooltip({
  89. super.key,
  90. this.message,
  91. this.richMessage,
  92. this.height,
  93. this.padding,
  94. this.margin,
  95. this.verticalOffset,
  96. this.preferBelow,
  97. this.excludeFromSemantics,
  98. this.decoration,
  99. this.textStyle,
  100. this.textAlign,
  101. this.waitDuration,
  102. this.showDuration,
  103. this.triggerMode,
  104. this.enableFeedback,
  105. this.onTriggered,
  106. this.child,
  107. }) : assert((message == null) != (richMessage == null), 'Either `message` or `richMessage` must be specified'),
  108. assert(
  109. richMessage == null || textStyle == null,
  110. 'If `richMessage` is specified, `textStyle` will have no effect. '
  111. 'If you wish to provide a `textStyle` for a rich tooltip, add the '
  112. '`textStyle` directly to the `richMessage` InlineSpan.',
  113. );
  114. /// The text to display in the tooltip.
  115. ///
  116. /// Only one of [message] and [richMessage] may be non-null.
  117. final String? message;
  118. /// The rich text to display in the tooltip.
  119. ///
  120. /// Only one of [message] and [richMessage] may be non-null.
  121. final InlineSpan? richMessage;
  122. /// The height of the tooltip's [child].
  123. ///
  124. /// If the [child] is null, then this is the tooltip's intrinsic height.
  125. final double? height;
  126. /// The amount of space by which to inset the tooltip's [child].
  127. ///
  128. /// On mobile, defaults to 16.0 logical pixels horizontally and 4.0 vertically.
  129. /// On desktop, defaults to 8.0 logical pixels horizontally and 4.0 vertically.
  130. final EdgeInsetsGeometry? padding;
  131. /// The empty space that surrounds the tooltip.
  132. ///
  133. /// Defines the tooltip's outer [Container.margin]. By default, a
  134. /// long tooltip will span the width of its window. If long enough,
  135. /// a tooltip might also span the window's height. This property allows
  136. /// one to define how much space the tooltip must be inset from the edges
  137. /// of their display window.
  138. ///
  139. /// If this property is null, then [TooltipThemeData.margin] is used.
  140. /// If [TooltipThemeData.margin] is also null, the default margin is
  141. /// 0.0 logical pixels on all sides.
  142. final EdgeInsetsGeometry? margin;
  143. /// The vertical gap between the widget and the displayed tooltip.
  144. ///
  145. /// When [preferBelow] is set to true and tooltips have sufficient space to
  146. /// display themselves, this property defines how much vertical space
  147. /// tooltips will position themselves under their corresponding widgets.
  148. /// Otherwise, tooltips will position themselves above their corresponding
  149. /// widgets with the given offset.
  150. final double? verticalOffset;
  151. /// Whether the tooltip defaults to being displayed below the widget.
  152. ///
  153. /// Defaults to true. If there is insufficient space to display the tooltip in
  154. /// the preferred direction, the tooltip will be displayed in the opposite
  155. /// direction.
  156. final bool? preferBelow;
  157. /// Whether the tooltip's [message] or [richMessage] should be excluded from
  158. /// the semantics tree.
  159. ///
  160. /// Defaults to false. A tooltip will add a [Semantics] label that is set to
  161. /// [CustomTooltip.message] if non-null, or the plain text value of
  162. /// [CustomTooltip.richMessage] otherwise. Set this property to true if the app is
  163. /// going to provide its own custom semantics label.
  164. final bool? excludeFromSemantics;
  165. /// The widget below this widget in the tree.
  166. ///
  167. /// {@macro flutter.widgets.ProxyWidget.child}
  168. final Widget? child;
  169. /// Specifies the tooltip's shape and background color.
  170. ///
  171. /// The tooltip shape defaults to a rounded rectangle with a border radius of
  172. /// 4.0. Tooltips will also default to an opacity of 90% and with the color
  173. /// [Colors.grey]\[700\] if [ThemeData.brightness] is [Brightness.dark], and
  174. /// [Colors.white] if it is [Brightness.light].
  175. final Decoration? decoration;
  176. /// The style to use for the message of the tooltip.
  177. ///
  178. /// If null, the message's [TextStyle] will be determined based on
  179. /// [ThemeData]. If [ThemeData.brightness] is set to [Brightness.dark],
  180. /// [TextTheme.bodyMedium] of [ThemeData.textTheme] will be used with
  181. /// [Colors.white]. Otherwise, if [ThemeData.brightness] is set to
  182. /// [Brightness.light], [TextTheme.bodyMedium] of [ThemeData.textTheme] will be
  183. /// used with [Colors.black].
  184. final TextStyle? textStyle;
  185. /// How the message of the tooltip is aligned horizontally.
  186. ///
  187. /// If this property is null, then [TooltipThemeData.textAlign] is used.
  188. /// If [TooltipThemeData.textAlign] is also null, the default value is
  189. /// [TextAlign.start].
  190. final TextAlign? textAlign;
  191. /// The length of time that a pointer must hover over a tooltip's widget
  192. /// before the tooltip will be shown.
  193. ///
  194. /// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
  195. final Duration? waitDuration;
  196. /// The length of time that the tooltip will be shown after a long press is
  197. /// released (if triggerMode is [TooltipTriggerMode.longPress]) or a tap is
  198. /// released (if triggerMode is [TooltipTriggerMode.tap]) or mouse pointer
  199. /// exits the widget.
  200. ///
  201. /// Defaults to 1.5 seconds for long press and tap released or 0.1 seconds
  202. /// for mouse pointer exits the widget.
  203. final Duration? showDuration;
  204. /// The [TooltipTriggerMode] that will show the tooltip.
  205. ///
  206. /// If this property is null, then [TooltipThemeData.triggerMode] is used.
  207. /// If [TooltipThemeData.triggerMode] is also null, the default mode is
  208. /// [TooltipTriggerMode.longPress].
  209. final TooltipTriggerMode? triggerMode;
  210. /// Whether the tooltip should provide acoustic and/or haptic feedback.
  211. ///
  212. /// For example, on Android a tap will produce a clicking sound and a
  213. /// long-press will produce a short vibration, when feedback is enabled.
  214. ///
  215. /// When null, the default value is true.
  216. ///
  217. /// See also:
  218. ///
  219. /// * [Feedback], for providing platform-specific feedback to certain actions.
  220. final bool? enableFeedback;
  221. /// Called when the Tooltip is triggered.
  222. ///
  223. /// The tooltip is triggered after a tap when [triggerMode] is [TooltipTriggerMode.tap]
  224. /// or after a long press when [triggerMode] is [TooltipTriggerMode.longPress].
  225. final TooltipTriggeredCallback? onTriggered;
  226. static final List<CustomTooltipState> _openedTooltips = <CustomTooltipState>[];
  227. // Causes any current tooltips to be concealed. Only called for mouse hover enter
  228. // detections. Won't conceal the supplied tooltip.
  229. static void _concealOtherTooltips(CustomTooltipState current) {
  230. if (_openedTooltips.isNotEmpty) {
  231. // Avoid concurrent modification.
  232. final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
  233. for (final CustomTooltipState state in openedTooltips) {
  234. if (state == current) {
  235. continue;
  236. }
  237. state._concealTooltip();
  238. }
  239. }
  240. }
  241. // Causes the most recently concealed tooltip to be revealed. Only called for mouse
  242. // hover exit detections.
  243. static void _revealLastTooltip() {
  244. if (_openedTooltips.isNotEmpty) {
  245. _openedTooltips.last._revealTooltip();
  246. }
  247. }
  248. /// Dismiss all of the tooltips that are currently shown on the screen.
  249. ///
  250. /// This method returns true if it successfully dismisses the tooltips. It
  251. /// returns false if there is no tooltip shown on the screen.
  252. static bool dismissAllToolTips() {
  253. if (_openedTooltips.isNotEmpty) {
  254. // Avoid concurrent modification.
  255. final List<CustomTooltipState> openedTooltips = _openedTooltips.toList();
  256. for (final CustomTooltipState state in openedTooltips) {
  257. state._dismissTooltip(immediately: true);
  258. }
  259. return true;
  260. }
  261. return false;
  262. }
  263. @override
  264. State<CustomTooltip> createState() => CustomTooltipState();
  265. @override
  266. void debugFillProperties(DiagnosticPropertiesBuilder properties) {
  267. super.debugFillProperties(properties);
  268. properties.add(StringProperty(
  269. 'message',
  270. message,
  271. showName: message == null,
  272. defaultValue: message == null ? null : kNoDefaultValue,
  273. ));
  274. properties.add(StringProperty(
  275. 'richMessage',
  276. richMessage?.toPlainText(),
  277. showName: richMessage == null,
  278. defaultValue: richMessage == null ? null : kNoDefaultValue,
  279. ));
  280. properties.add(DoubleProperty('height', height, defaultValue: null));
  281. properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: null));
  282. properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin, defaultValue: null));
  283. properties.add(DoubleProperty('vertical offset', verticalOffset, defaultValue: null));
  284. properties.add(FlagProperty('position', value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true));
  285. properties.add(FlagProperty('semantics', value: excludeFromSemantics, ifTrue: 'excluded', showName: true));
  286. properties.add(DiagnosticsProperty<Duration>('wait duration', waitDuration, defaultValue: null));
  287. properties.add(DiagnosticsProperty<Duration>('show duration', showDuration, defaultValue: null));
  288. properties.add(DiagnosticsProperty<TooltipTriggerMode>('triggerMode', triggerMode, defaultValue: null));
  289. properties.add(FlagProperty('enableFeedback', value: enableFeedback, ifTrue: 'true', showName: true));
  290. properties.add(DiagnosticsProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
  291. }
  292. }
  293. /// Contains the state for a [CustomTooltip].
  294. ///
  295. /// This class can be used to programmatically show the Tooltip, see the
  296. /// [ensureTooltipVisible] method.
  297. class CustomTooltipState extends State<CustomTooltip> with SingleTickerProviderStateMixin {
  298. final double _defaultVerticalOffset = 24.s;
  299. static const bool _defaultPreferBelow = true;
  300. static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
  301. static const Duration _fadeInDuration = Duration(milliseconds: 150);
  302. static const Duration _fadeOutDuration = Duration(milliseconds: 75);
  303. static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
  304. static const Duration _defaultHoverShowDuration = Duration(milliseconds: 100);
  305. static const Duration _defaultWaitDuration = Duration.zero;
  306. static const bool _defaultExcludeFromSemantics = false;
  307. static const TooltipTriggerMode _defaultTriggerMode = TooltipTriggerMode.longPress;
  308. static const bool _defaultEnableFeedback = true;
  309. static const TextAlign _defaultTextAlign = TextAlign.start;
  310. late double _height;
  311. late EdgeInsetsGeometry _padding;
  312. late EdgeInsetsGeometry _margin;
  313. late Decoration _decoration;
  314. late TextStyle _textStyle;
  315. late TextAlign _textAlign;
  316. late double _verticalOffset;
  317. late bool _preferBelow;
  318. late bool _excludeFromSemantics;
  319. late AnimationController _controller;
  320. OverlayEntry? _entry;
  321. Timer? _dismissTimer;
  322. Timer? _showTimer;
  323. late Duration _showDuration;
  324. late Duration _hoverShowDuration;
  325. late Duration _waitDuration;
  326. late bool _mouseIsConnected;
  327. bool _pressActivated = false;
  328. late TooltipTriggerMode _triggerMode;
  329. late bool _enableFeedback;
  330. late bool _isConcealed;
  331. late bool _forceRemoval;
  332. late bool _visible;
  333. /// The plain text message for this tooltip.
  334. ///
  335. /// This value will either come from [widget.message] or [widget.richMessage].
  336. String get _tooltipMessage => widget.message ?? widget.richMessage!.toPlainText();
  337. @override
  338. void initState() {
  339. super.initState();
  340. _isConcealed = false;
  341. _forceRemoval = false;
  342. _mouseIsConnected = RendererBinding.instance.mouseTracker.mouseIsConnected;
  343. _controller = AnimationController(
  344. duration: _fadeInDuration,
  345. reverseDuration: _fadeOutDuration,
  346. vsync: this,
  347. )
  348. ..addStatusListener(_handleStatusChanged);
  349. // Listen to see when a mouse is added.
  350. RendererBinding.instance.mouseTracker.addListener(_handleMouseTrackerChange);
  351. // Listen to global pointer events so that we can hide a tooltip immediately
  352. // if some other control is clicked on.
  353. GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
  354. }
  355. @override
  356. void didChangeDependencies() {
  357. super.didChangeDependencies();
  358. _visible = TooltipVisibility.of(context);
  359. }
  360. // https://material.io/components/tooltips#specs
  361. double _getDefaultTooltipHeight() {
  362. final ThemeData theme = Theme.of(context);
  363. switch (theme.platform) {
  364. case TargetPlatform.macOS:
  365. case TargetPlatform.linux:
  366. case TargetPlatform.windows:
  367. return 24.s;
  368. case TargetPlatform.android:
  369. case TargetPlatform.fuchsia:
  370. case TargetPlatform.iOS:
  371. return 32.s;
  372. }
  373. }
  374. EdgeInsets _getDefaultPadding() {
  375. final ThemeData theme = Theme.of(context);
  376. switch (theme.platform) {
  377. case TargetPlatform.macOS:
  378. case TargetPlatform.linux:
  379. case TargetPlatform.windows:
  380. return EdgeInsets.symmetric(horizontal: 8.s, vertical: 4.s);
  381. case TargetPlatform.android:
  382. case TargetPlatform.fuchsia:
  383. case TargetPlatform.iOS:
  384. return EdgeInsets.symmetric(horizontal: 16.s, vertical: 4.s);
  385. }
  386. }
  387. double _getDefaultFontSize() {
  388. final ThemeData theme = Theme.of(context);
  389. switch (theme.platform) {
  390. case TargetPlatform.macOS:
  391. case TargetPlatform.linux:
  392. case TargetPlatform.windows:
  393. return 12.s;
  394. case TargetPlatform.android:
  395. case TargetPlatform.fuchsia:
  396. case TargetPlatform.iOS:
  397. return 14.s;
  398. }
  399. }
  400. // Forces a rebuild if a mouse has been added or removed.
  401. void _handleMouseTrackerChange() {
  402. if (!mounted) {
  403. return;
  404. }
  405. final bool mouseIsConnected = RendererBinding.instance.mouseTracker.mouseIsConnected;
  406. if (mouseIsConnected != _mouseIsConnected) {
  407. setState(() {
  408. _mouseIsConnected = mouseIsConnected;
  409. });
  410. }
  411. }
  412. void _handleStatusChanged(AnimationStatus status) {
  413. // If this tip is concealed, don't remove it, even if it is dismissed, so that we can
  414. // reveal it later, unless it has explicitly been hidden with _dismissTooltip.
  415. if (status == AnimationStatus.dismissed && (_forceRemoval || !_isConcealed)) {
  416. _removeEntry();
  417. }
  418. }
  419. void _dismissTooltip({ bool immediately = false }) {
  420. _showTimer?.cancel();
  421. _showTimer = null;
  422. if (immediately) {
  423. _removeEntry();
  424. return;
  425. }
  426. // So it will be removed when it's done reversing, regardless of whether it is
  427. // still concealed or not.
  428. _forceRemoval = true;
  429. if (_pressActivated) {
  430. _dismissTimer ??= Timer(_showDuration, _controller.reverse);
  431. } else {
  432. _dismissTimer ??= Timer(_hoverShowDuration, _controller.reverse);
  433. }
  434. _pressActivated = false;
  435. }
  436. void _showTooltip({ bool immediately = false }) {
  437. _dismissTimer?.cancel();
  438. _dismissTimer = null;
  439. if (immediately) {
  440. ensureTooltipVisible();
  441. return;
  442. }
  443. _showTimer ??= Timer(_waitDuration, ensureTooltipVisible);
  444. }
  445. void _concealTooltip() {
  446. if (_isConcealed || _forceRemoval) {
  447. // Already concealed, or it's being removed.
  448. return;
  449. }
  450. _isConcealed = true;
  451. _dismissTimer?.cancel();
  452. _dismissTimer = null;
  453. _showTimer?.cancel();
  454. _showTimer = null;
  455. if (_entry != null) {
  456. _entry!.remove();
  457. }
  458. _controller.reverse();
  459. }
  460. void _revealTooltip() {
  461. if (!_isConcealed) {
  462. // Already uncovered.
  463. return;
  464. }
  465. _isConcealed = false;
  466. _dismissTimer?.cancel();
  467. _dismissTimer = null;
  468. _showTimer?.cancel();
  469. _showTimer = null;
  470. if (!_entry!.mounted) {
  471. final OverlayState overlayState = Overlay.of(
  472. context,
  473. debugRequiredFor: widget,
  474. );
  475. overlayState.insert(_entry!);
  476. }
  477. SemanticsService.tooltip(_tooltipMessage);
  478. _controller.forward();
  479. }
  480. /// Shows the tooltip if it is not already visible.
  481. ///
  482. /// Returns `false` when the tooltip shouldn't be shown or when the tooltip
  483. /// was already visible.
  484. bool ensureTooltipVisible() {
  485. if (!_visible || !mounted) {
  486. return false;
  487. }
  488. _showTimer?.cancel();
  489. _showTimer = null;
  490. _forceRemoval = false;
  491. if (_isConcealed) {
  492. if (_mouseIsConnected) {
  493. CustomTooltip._concealOtherTooltips(this);
  494. }
  495. _revealTooltip();
  496. return true;
  497. }
  498. if (_entry != null) {
  499. // Stop trying to hide, if we were.
  500. _dismissTimer?.cancel();
  501. _dismissTimer = null;
  502. _controller.forward();
  503. return false; // Already visible.
  504. }
  505. _createNewEntry();
  506. _controller.forward();
  507. return true;
  508. }
  509. static final Set<CustomTooltipState> _mouseIn = <CustomTooltipState>{};
  510. void _handleMouseEnter() {
  511. if (mounted) {
  512. _showTooltip();
  513. }
  514. }
  515. void _handleMouseExit({bool immediately = false}) {
  516. if (mounted) {
  517. // If the tip is currently covered, we can just remove it without waiting.
  518. _dismissTooltip(immediately: _isConcealed || immediately);
  519. }
  520. }
  521. void _createNewEntry() {
  522. final OverlayState overlayState = Overlay.of(
  523. context,
  524. debugRequiredFor: widget,
  525. );
  526. final RenderBox box = context.findRenderObject()! as RenderBox;
  527. final Offset target = box.localToGlobal(
  528. box.size.center(Offset.zero),
  529. ancestor: overlayState.context.findRenderObject(),
  530. );
  531. // We create this widget outside of the overlay entry's builder to prevent
  532. // updated values from happening to leak into the overlay when the overlay
  533. // rebuilds.
  534. final Widget overlay = Directionality(
  535. textDirection: Directionality.of(context),
  536. child: _TooltipOverlay(
  537. richMessage: widget.richMessage ?? TextSpan(text: widget.message),
  538. height: _height,
  539. padding: _padding,
  540. margin: _margin,
  541. onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null,
  542. onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null,
  543. decoration: _decoration,
  544. textStyle: _textStyle,
  545. textAlign: _textAlign,
  546. animation: CurvedAnimation(
  547. parent: _controller,
  548. curve: Curves.fastOutSlowIn,
  549. ),
  550. target: target,
  551. verticalOffset: _verticalOffset,
  552. preferBelow: _preferBelow,
  553. ),
  554. );
  555. _entry = OverlayEntry(builder: (BuildContext context) => overlay);
  556. _isConcealed = false;
  557. overlayState.insert(_entry!);
  558. SemanticsService.tooltip(_tooltipMessage);
  559. if (_mouseIsConnected) {
  560. // Hovered tooltips shouldn't show more than one at once. For example, a chip with
  561. // a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
  562. // at the same time.
  563. CustomTooltip._concealOtherTooltips(this);
  564. }
  565. assert(!CustomTooltip._openedTooltips.contains(this));
  566. CustomTooltip._openedTooltips.add(this);
  567. }
  568. void _removeEntry() {
  569. CustomTooltip._openedTooltips.remove(this);
  570. _mouseIn.remove(this);
  571. _dismissTimer?.cancel();
  572. _dismissTimer = null;
  573. _showTimer?.cancel();
  574. _showTimer = null;
  575. if (!_isConcealed) {
  576. _entry?.remove();
  577. }
  578. _isConcealed = false;
  579. _entry = null;
  580. if (_mouseIsConnected) {
  581. CustomTooltip._revealLastTooltip();
  582. }
  583. }
  584. void _handlePointerEvent(PointerEvent event) {
  585. if (_entry == null) {
  586. return;
  587. }
  588. if (event is PointerUpEvent || event is PointerCancelEvent) {
  589. _handleMouseExit();
  590. } else if (event is PointerDownEvent) {
  591. _handleMouseExit(immediately: true);
  592. }
  593. }
  594. @override
  595. void deactivate() {
  596. if (_entry != null) {
  597. _dismissTooltip(immediately: true);
  598. }
  599. _showTimer?.cancel();
  600. super.deactivate();
  601. }
  602. @override
  603. void dispose() {
  604. GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent);
  605. RendererBinding.instance.mouseTracker.removeListener(_handleMouseTrackerChange);
  606. _removeEntry();
  607. _controller.dispose();
  608. super.dispose();
  609. }
  610. void _handlePress() {
  611. _pressActivated = true;
  612. final bool tooltipCreated = ensureTooltipVisible();
  613. if (tooltipCreated && _enableFeedback) {
  614. if (_triggerMode == TooltipTriggerMode.longPress) {
  615. Feedback.forLongPress(context);
  616. } else {
  617. Feedback.forTap(context);
  618. }
  619. }
  620. widget.onTriggered?.call();
  621. }
  622. void _handleTap() {
  623. _handlePress();
  624. // When triggerMode is not [TooltipTriggerMode.tap] the tooltip is dismissed
  625. // by _handlePointerEvent, which listens to the global pointer events.
  626. // When triggerMode is [TooltipTriggerMode.tap] and the Tooltip GestureDetector
  627. // competes with other GestureDetectors, the disambiguation process will complete
  628. // after the global pointer event is received. As we can't rely on the global
  629. // pointer events to dismiss the Tooltip, we have to call _handleMouseExit
  630. // to dismiss the tooltip after _showDuration expired.
  631. _handleMouseExit();
  632. }
  633. @override
  634. Widget build(BuildContext context) {
  635. // If message is empty then no need to create a tooltip overlay to show
  636. // the empty black container so just return the wrapped child as is or
  637. // empty container if child is not specified.
  638. if (_tooltipMessage.isEmpty) {
  639. return widget.child ?? const SizedBox.shrink();
  640. }
  641. assert(debugCheckHasOverlay(context));
  642. final ThemeData theme = Theme.of(context);
  643. final TooltipThemeData tooltipTheme = TooltipTheme.of(context);
  644. final TextStyle defaultTextStyle;
  645. final BoxDecoration defaultDecoration;
  646. if (theme.brightness == Brightness.dark) {
  647. defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
  648. color: Colors.black,
  649. fontSize: _getDefaultFontSize(),
  650. );
  651. defaultDecoration = BoxDecoration(
  652. color: Colors.white.withOpacity(0.9),
  653. borderRadius: const BorderRadius.all(Radius.circular(4)),
  654. );
  655. } else {
  656. defaultTextStyle = theme.textTheme.bodyMedium!.copyWith(
  657. color: Colors.white,
  658. fontSize: _getDefaultFontSize(),
  659. );
  660. defaultDecoration = BoxDecoration(
  661. color: Colors.grey[700]!.withOpacity(0.9),
  662. borderRadius: const BorderRadius.all(Radius.circular(4)),
  663. );
  664. }
  665. _height = widget.height ?? tooltipTheme.height ?? _getDefaultTooltipHeight();
  666. _padding = widget.padding ?? tooltipTheme.padding ?? _getDefaultPadding();
  667. _margin = widget.margin ?? tooltipTheme.margin ?? _defaultMargin;
  668. _verticalOffset = widget.verticalOffset ?? tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
  669. _preferBelow = widget.preferBelow ?? tooltipTheme.preferBelow ?? _defaultPreferBelow;
  670. _excludeFromSemantics = widget.excludeFromSemantics ?? tooltipTheme.excludeFromSemantics ?? _defaultExcludeFromSemantics;
  671. _decoration = widget.decoration ?? tooltipTheme.decoration ?? defaultDecoration;
  672. _textStyle = widget.textStyle ?? tooltipTheme.textStyle ?? defaultTextStyle;
  673. _textAlign = widget.textAlign ?? tooltipTheme.textAlign ?? _defaultTextAlign;
  674. _waitDuration = widget.waitDuration ?? tooltipTheme.waitDuration ?? _defaultWaitDuration;
  675. _showDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultShowDuration;
  676. _hoverShowDuration = widget.showDuration ?? tooltipTheme.showDuration ?? _defaultHoverShowDuration;
  677. _triggerMode = widget.triggerMode ?? tooltipTheme.triggerMode ?? _defaultTriggerMode;
  678. _enableFeedback = widget.enableFeedback ?? tooltipTheme.enableFeedback ?? _defaultEnableFeedback;
  679. Widget result = Semantics(
  680. tooltip: _excludeFromSemantics
  681. ? null
  682. : _tooltipMessage,
  683. child: widget.child,
  684. );
  685. // Only check for gestures if tooltip should be visible.
  686. if (_visible) {
  687. result = GestureDetector(
  688. behavior: HitTestBehavior.opaque,
  689. onLongPress: (_triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
  690. onTap: (_triggerMode == TooltipTriggerMode.tap) ? _handleTap : null,
  691. excludeFromSemantics: true,
  692. child: result,
  693. );
  694. // Only check for hovering if there is a mouse connected.
  695. if (_mouseIsConnected) {
  696. result = MouseRegion(
  697. onEnter: (_) => _handleMouseEnter(),
  698. onExit: (_) => _handleMouseExit(),
  699. child: result,
  700. );
  701. }
  702. }
  703. return result;
  704. }
  705. }
  706. /// A delegate for computing the layout of a tooltip to be displayed above or
  707. /// below a target specified in the global coordinate system.
  708. class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
  709. /// Creates a delegate for computing the layout of a tooltip.
  710. ///
  711. /// The arguments must not be null.
  712. _TooltipPositionDelegate({
  713. required this.target,
  714. required this.verticalOffset,
  715. required this.preferBelow,
  716. }) : assert(target != null),
  717. assert(verticalOffset != null),
  718. assert(preferBelow != null);
  719. /// The offset of the target the tooltip is positioned near in the global
  720. /// coordinate system.
  721. final Offset target;
  722. /// The amount of vertical distance between the target and the displayed
  723. /// tooltip.
  724. final double verticalOffset;
  725. /// Whether the tooltip is displayed below its widget by default.
  726. ///
  727. /// If there is insufficient space to display the tooltip in the preferred
  728. /// direction, the tooltip will be displayed in the opposite direction.
  729. final bool preferBelow;
  730. @override
  731. BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
  732. @override
  733. Offset getPositionForChild(Size size, Size childSize) {
  734. return positionDependentBox(
  735. size: size,
  736. childSize: childSize,
  737. target: target,
  738. verticalOffset: verticalOffset,
  739. preferBelow: preferBelow,
  740. );
  741. }
  742. @override
  743. bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
  744. return target != oldDelegate.target
  745. || verticalOffset != oldDelegate.verticalOffset
  746. || preferBelow != oldDelegate.preferBelow;
  747. }
  748. }
  749. class _TooltipOverlay extends StatelessWidget {
  750. const _TooltipOverlay({
  751. required this.height,
  752. required this.richMessage,
  753. this.padding,
  754. this.margin,
  755. this.decoration,
  756. this.textStyle,
  757. this.textAlign,
  758. required this.animation,
  759. required this.target,
  760. required this.verticalOffset,
  761. required this.preferBelow,
  762. this.onEnter,
  763. this.onExit,
  764. });
  765. final InlineSpan richMessage;
  766. final double height;
  767. final EdgeInsetsGeometry? padding;
  768. final EdgeInsetsGeometry? margin;
  769. final Decoration? decoration;
  770. final TextStyle? textStyle;
  771. final TextAlign? textAlign;
  772. final Animation<double> animation;
  773. final Offset target;
  774. final double verticalOffset;
  775. final bool preferBelow;
  776. final PointerEnterEventListener? onEnter;
  777. final PointerExitEventListener? onExit;
  778. @override
  779. Widget build(BuildContext context) {
  780. Widget result = IgnorePointer(
  781. child: FadeTransition(
  782. opacity: animation,
  783. child: ConstrainedBox(
  784. constraints: BoxConstraints(minHeight: height),
  785. child: DefaultTextStyle(
  786. style: Theme.of(context).textTheme.bodyMedium!,
  787. child: Container(
  788. decoration: decoration,
  789. padding: padding,
  790. margin: margin,
  791. child: Center(
  792. widthFactor: 1.0,
  793. heightFactor: 1.0,
  794. child: Text.rich(
  795. richMessage,
  796. style: textStyle,
  797. textAlign: textAlign,
  798. ),
  799. ),
  800. ),
  801. ),
  802. ),
  803. )
  804. );
  805. if (onEnter != null || onExit != null) {
  806. result = MouseRegion(
  807. onEnter: onEnter,
  808. onExit: onExit,
  809. child: result,
  810. );
  811. }
  812. return Positioned.fill(
  813. bottom: MediaQuery.maybeOf(context)?.viewInsets.bottom ?? 0.0,
  814. child: CustomSingleChildLayout(
  815. delegate: _TooltipPositionDelegate(
  816. target: target,
  817. verticalOffset: verticalOffset,
  818. preferBelow: preferBelow,
  819. ),
  820. child: result,
  821. ),
  822. );
  823. }
  824. }