1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:async'; |
6 | |
7 | import 'package:flutter/scheduler.dart'; |
8 | import 'package:flutter/services.dart'; |
9 | |
10 | import 'actions.dart'; |
11 | import 'basic.dart'; |
12 | import 'container.dart'; |
13 | import 'editable_text.dart'; |
14 | import 'focus_manager.dart'; |
15 | import 'framework.dart'; |
16 | import 'inherited_notifier.dart'; |
17 | import 'overlay.dart'; |
18 | import 'shortcuts.dart'; |
19 | import 'tap_region.dart'; |
20 | |
21 | // Examples can assume: |
22 | // late BuildContext context; |
23 | |
24 | /// The type of the [RawAutocomplete] callback which computes the list of |
25 | /// optional completions for the widget's field, based on the text the user has |
26 | /// entered so far. |
27 | /// |
28 | /// See also: |
29 | /// |
30 | /// * [RawAutocomplete.optionsBuilder], which is of this type. |
31 | typedef AutocompleteOptionsBuilder<T extends Object> = FutureOr<Iterable<T>> Function(TextEditingValue textEditingValue); |
32 | |
33 | /// The type of the callback used by the [RawAutocomplete] widget to indicate |
34 | /// that the user has selected an option. |
35 | /// |
36 | /// See also: |
37 | /// |
38 | /// * [RawAutocomplete.onSelected], which is of this type. |
39 | typedef AutocompleteOnSelected<T extends Object> = void Function(T option); |
40 | |
41 | /// The type of the [RawAutocomplete] callback which returns a [Widget] that |
42 | /// displays the specified [options] and calls [onSelected] if the user |
43 | /// selects an option. |
44 | /// |
45 | /// The returned widget from this callback will be wrapped in an |
46 | /// [AutocompleteHighlightedOption] inherited widget. This will allow |
47 | /// this callback to determine which option is currently highlighted for |
48 | /// keyboard navigation. |
49 | /// |
50 | /// See also: |
51 | /// |
52 | /// * [RawAutocomplete.optionsViewBuilder], which is of this type. |
53 | typedef AutocompleteOptionsViewBuilder<T extends Object> = Widget Function( |
54 | BuildContext context, |
55 | AutocompleteOnSelected<T> onSelected, |
56 | Iterable<T> options, |
57 | ); |
58 | |
59 | /// The type of the Autocomplete callback which returns the widget that |
60 | /// contains the input [TextField] or [TextFormField]. |
61 | /// |
62 | /// See also: |
63 | /// |
64 | /// * [RawAutocomplete.fieldViewBuilder], which is of this type. |
65 | typedef AutocompleteFieldViewBuilder = Widget Function( |
66 | BuildContext context, |
67 | TextEditingController textEditingController, |
68 | FocusNode focusNode, |
69 | VoidCallback onFieldSubmitted, |
70 | ); |
71 | |
72 | /// The type of the [RawAutocomplete] callback that converts an option value to |
73 | /// a string which can be displayed in the widget's options menu. |
74 | /// |
75 | /// See also: |
76 | /// |
77 | /// * [RawAutocomplete.displayStringForOption], which is of this type. |
78 | typedef AutocompleteOptionToString<T extends Object> = String Function(T option); |
79 | |
80 | /// A direction in which to open the options-view overlay. |
81 | /// |
82 | /// See also: |
83 | /// |
84 | /// * [RawAutocomplete.optionsViewOpenDirection], which is of this type. |
85 | /// * [RawAutocomplete.optionsViewBuilder] to specify how to build the |
86 | /// selectable-options widget. |
87 | /// * [RawAutocomplete.fieldViewBuilder] to optionally specify how to build the |
88 | /// corresponding field widget. |
89 | enum OptionsViewOpenDirection { |
90 | /// Open upward. |
91 | /// |
92 | /// The bottom edge of the options view will align with the top edge |
93 | /// of the text field built by [RawAutocomplete.fieldViewBuilder]. |
94 | up, |
95 | |
96 | /// Open downward. |
97 | /// |
98 | /// The top edge of the options view will align with the bottom edge |
99 | /// of the text field built by [RawAutocomplete.fieldViewBuilder]. |
100 | down, |
101 | } |
102 | |
103 | // TODO(justinmc): Mention AutocompleteCupertino when it is implemented. |
104 | /// {@template flutter.widgets.RawAutocomplete.RawAutocomplete} |
105 | /// A widget for helping the user make a selection by entering some text and |
106 | /// choosing from among a list of options. |
107 | /// |
108 | /// The user's text input is received in a field built with the |
109 | /// [fieldViewBuilder] parameter. The options to be displayed are determined |
110 | /// using [optionsBuilder] and rendered with [optionsViewBuilder]. |
111 | /// {@endtemplate} |
112 | /// |
113 | /// This is a core framework widget with very basic UI. |
114 | /// |
115 | /// {@tool dartpad} |
116 | /// This example shows how to create a very basic autocomplete widget using the |
117 | /// [fieldViewBuilder] and [optionsViewBuilder] parameters. |
118 | /// |
119 | /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.0.dart ** |
120 | /// {@end-tool} |
121 | /// |
122 | /// The type parameter T represents the type of the options. Most commonly this |
123 | /// is a String, as in the example above. However, it's also possible to use |
124 | /// another type with a `toString` method, or a custom [displayStringForOption]. |
125 | /// Options will be compared using `==`, so it may be beneficial to override |
126 | /// [Object.==] and [Object.hashCode] for custom types. |
127 | /// |
128 | /// {@tool dartpad} |
129 | /// This example is similar to the previous example, but it uses a custom T data |
130 | /// type instead of directly using String. |
131 | /// |
132 | /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.1.dart ** |
133 | /// {@end-tool} |
134 | /// |
135 | /// {@tool dartpad} |
136 | /// This example shows the use of RawAutocomplete in a form. |
137 | /// |
138 | /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.2.dart ** |
139 | /// {@end-tool} |
140 | /// |
141 | /// See also: |
142 | /// |
143 | /// * [Autocomplete], which is a Material-styled implementation that is based |
144 | /// on RawAutocomplete. |
145 | class RawAutocomplete<T extends Object> extends StatefulWidget { |
146 | /// Create an instance of RawAutocomplete. |
147 | /// |
148 | /// [displayStringForOption], [optionsBuilder] and [optionsViewBuilder] must |
149 | /// not be null. |
150 | const RawAutocomplete({ |
151 | super.key, |
152 | required this.optionsViewBuilder, |
153 | required this.optionsBuilder, |
154 | this.optionsViewOpenDirection = OptionsViewOpenDirection.down, |
155 | this.displayStringForOption = defaultStringForOption, |
156 | this.fieldViewBuilder, |
157 | this.focusNode, |
158 | this.onSelected, |
159 | this.textEditingController, |
160 | this.initialValue, |
161 | }) : assert( |
162 | fieldViewBuilder != null |
163 | || (key != null && focusNode != null && textEditingController != null), |
164 | 'Pass in a fieldViewBuilder, or otherwise create a separate field and pass in the FocusNode, TextEditingController, and a key. Use the key with RawAutocomplete.onFieldSubmitted.' , |
165 | ), |
166 | assert((focusNode == null) == (textEditingController == null)), |
167 | assert( |
168 | !(textEditingController != null && initialValue != null), |
169 | 'textEditingController and initialValue cannot be simultaneously defined.' , |
170 | ); |
171 | |
172 | /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder} |
173 | /// Builds the field whose input is used to get the options. |
174 | /// |
175 | /// Pass the provided [TextEditingController] to the field built here so that |
176 | /// RawAutocomplete can listen for changes. |
177 | /// {@endtemplate} |
178 | /// |
179 | /// If this parameter is null, then a [SizedBox.shrink] is built instead. |
180 | /// For how that pattern can be useful, see [textEditingController]. |
181 | final AutocompleteFieldViewBuilder? fieldViewBuilder; |
182 | |
183 | /// The [FocusNode] that is used for the text field. |
184 | /// |
185 | /// {@template flutter.widgets.RawAutocomplete.split} |
186 | /// The main purpose of this parameter is to allow the use of a separate text |
187 | /// field located in another part of the widget tree instead of the text |
188 | /// field built by [fieldViewBuilder]. For example, it may be desirable to |
189 | /// place the text field in the AppBar and the options below in the main body. |
190 | /// |
191 | /// When following this pattern, [fieldViewBuilder] can be omitted, |
192 | /// so that a text field is not drawn where it would normally be. |
193 | /// A separate text field can be created elsewhere, and a |
194 | /// FocusNode and TextEditingController can be passed both to that text field |
195 | /// and to RawAutocomplete. |
196 | /// |
197 | /// {@tool dartpad} |
198 | /// This examples shows how to create an autocomplete widget with the text |
199 | /// field in the AppBar and the results in the main body of the app. |
200 | /// |
201 | /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart ** |
202 | /// {@end-tool} |
203 | /// {@endtemplate} |
204 | /// |
205 | /// If this parameter is not null, then [textEditingController] must also be |
206 | /// not null. |
207 | final FocusNode? focusNode; |
208 | |
209 | /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} |
210 | /// Builds the selectable options widgets from a list of options objects. |
211 | /// |
212 | /// The options are displayed floating below or above the field using a |
213 | /// [CompositedTransformFollower] inside of an [Overlay], not at the same |
214 | /// place in the widget tree as [RawAutocomplete]. To control whether it opens |
215 | /// upward or downward, use [optionsViewOpenDirection]. |
216 | /// |
217 | /// In order to track which item is highlighted by keyboard navigation, the |
218 | /// resulting options will be wrapped in an inherited |
219 | /// [AutocompleteHighlightedOption] widget. |
220 | /// Inside this callback, the index of the highlighted option can be obtained |
221 | /// from [AutocompleteHighlightedOption.of] to display the highlighted option |
222 | /// with a visual highlight to indicate it will be the option selected from |
223 | /// the keyboard. |
224 | /// |
225 | /// {@endtemplate} |
226 | final AutocompleteOptionsViewBuilder<T> optionsViewBuilder; |
227 | |
228 | /// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection} |
229 | /// The direction in which to open the options-view overlay. |
230 | /// |
231 | /// Defaults to [OptionsViewOpenDirection.down]. |
232 | /// {@endtemplate} |
233 | final OptionsViewOpenDirection optionsViewOpenDirection; |
234 | |
235 | /// {@template flutter.widgets.RawAutocomplete.displayStringForOption} |
236 | /// Returns the string to display in the field when the option is selected. |
237 | /// |
238 | /// This is useful when using a custom T type and the string to display is |
239 | /// different than the string to search by. |
240 | /// |
241 | /// If not provided, will use `option.toString()`. |
242 | /// {@endtemplate} |
243 | final AutocompleteOptionToString<T> displayStringForOption; |
244 | |
245 | /// {@template flutter.widgets.RawAutocomplete.onSelected} |
246 | /// Called when an option is selected by the user. |
247 | /// {@endtemplate} |
248 | final AutocompleteOnSelected<T>? onSelected; |
249 | |
250 | /// {@template flutter.widgets.RawAutocomplete.optionsBuilder} |
251 | /// A function that returns the current selectable options objects given the |
252 | /// current TextEditingValue. |
253 | /// {@endtemplate} |
254 | final AutocompleteOptionsBuilder<T> optionsBuilder; |
255 | |
256 | /// The [TextEditingController] that is used for the text field. |
257 | /// |
258 | /// {@macro flutter.widgets.RawAutocomplete.split} |
259 | /// |
260 | /// If this parameter is not null, then [focusNode] must also be not null. |
261 | final TextEditingController? textEditingController; |
262 | |
263 | /// {@template flutter.widgets.RawAutocomplete.initialValue} |
264 | /// The initial value to use for the text field. |
265 | /// {@endtemplate} |
266 | /// |
267 | /// Setting the initial value does not notify [textEditingController]'s |
268 | /// listeners, and thus will not cause the options UI to appear. |
269 | /// |
270 | /// This parameter is ignored if [textEditingController] is defined. |
271 | final TextEditingValue? initialValue; |
272 | |
273 | /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the |
274 | /// RawAutocomplete widget indicated by the given [GlobalKey]. |
275 | /// |
276 | /// This is not typically used unless a custom field is implemented instead of |
277 | /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted |
278 | /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When |
279 | /// not using fieldViewBuilder, the same callback can be called by using this |
280 | /// static method. |
281 | /// |
282 | /// See also: |
283 | /// |
284 | /// * [focusNode] and [textEditingController], which contain a code example |
285 | /// showing how to create a separate field outside of fieldViewBuilder. |
286 | static void onFieldSubmitted<T extends Object>(GlobalKey key) { |
287 | final _RawAutocompleteState<T> rawAutocomplete = key.currentState! as _RawAutocompleteState<T>; |
288 | rawAutocomplete._onFieldSubmitted(); |
289 | } |
290 | |
291 | /// The default way to convert an option to a string in |
292 | /// [displayStringForOption]. |
293 | /// |
294 | /// Uses the `toString` method of the given `option`. |
295 | static String defaultStringForOption(Object? option) { |
296 | return option.toString(); |
297 | } |
298 | |
299 | @override |
300 | State<RawAutocomplete<T>> createState() => _RawAutocompleteState<T>(); |
301 | } |
302 | |
303 | class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> { |
304 | final GlobalKey _fieldKey = GlobalKey(); |
305 | final LayerLink _optionsLayerLink = LayerLink(); |
306 | late TextEditingController _textEditingController; |
307 | late FocusNode _focusNode; |
308 | late final Map<Type, Action<Intent>> _actionMap; |
309 | late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction; |
310 | late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction; |
311 | late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction; |
312 | Iterable<T> _options = Iterable<T>.empty(); |
313 | T? _selection; |
314 | bool _userHidOptions = false; |
315 | String _lastFieldText = '' ; |
316 | final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0); |
317 | |
318 | static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{ |
319 | SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(), |
320 | SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(), |
321 | }; |
322 | |
323 | // The OverlayEntry containing the options. |
324 | OverlayEntry? _floatingOptions; |
325 | |
326 | // True iff the state indicates that the options should be visible. |
327 | bool get _shouldShowOptions { |
328 | return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty; |
329 | } |
330 | |
331 | // Called when _textEditingController changes. |
332 | Future<void> _onChangedField() async { |
333 | final TextEditingValue value = _textEditingController.value; |
334 | final Iterable<T> options = await widget.optionsBuilder( |
335 | value, |
336 | ); |
337 | _options = options; |
338 | _updateHighlight(_highlightedOptionIndex.value); |
339 | if (_selection != null |
340 | && value.text != widget.displayStringForOption(_selection!)) { |
341 | _selection = null; |
342 | } |
343 | |
344 | // Make sure the options are no longer hidden if the content of the field |
345 | // changes (ignore selection changes). |
346 | if (value.text != _lastFieldText) { |
347 | _userHidOptions = false; |
348 | _lastFieldText = value.text; |
349 | } |
350 | _updateActions(); |
351 | _updateOverlay(); |
352 | } |
353 | |
354 | // Called when the field's FocusNode changes. |
355 | void _onChangedFocus() { |
356 | // Options should no longer be hidden when the field is re-focused. |
357 | _userHidOptions = !_focusNode.hasFocus; |
358 | _updateActions(); |
359 | _updateOverlay(); |
360 | } |
361 | |
362 | // Called from fieldViewBuilder when the user submits the field. |
363 | void _onFieldSubmitted() { |
364 | if (_options.isEmpty || _userHidOptions) { |
365 | return; |
366 | } |
367 | _select(_options.elementAt(_highlightedOptionIndex.value)); |
368 | } |
369 | |
370 | // Select the given option and update the widget. |
371 | void _select(T nextSelection) { |
372 | if (nextSelection == _selection) { |
373 | return; |
374 | } |
375 | _selection = nextSelection; |
376 | final String selectionString = widget.displayStringForOption(nextSelection); |
377 | _textEditingController.value = TextEditingValue( |
378 | selection: TextSelection.collapsed(offset: selectionString.length), |
379 | text: selectionString, |
380 | ); |
381 | _updateActions(); |
382 | _updateOverlay(); |
383 | widget.onSelected?.call(_selection!); |
384 | } |
385 | |
386 | void _updateHighlight(int newIndex) { |
387 | _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length; |
388 | } |
389 | |
390 | void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) { |
391 | if (_userHidOptions) { |
392 | _userHidOptions = false; |
393 | _updateActions(); |
394 | _updateOverlay(); |
395 | return; |
396 | } |
397 | _updateHighlight(_highlightedOptionIndex.value - 1); |
398 | } |
399 | |
400 | void _highlightNextOption(AutocompleteNextOptionIntent intent) { |
401 | if (_userHidOptions) { |
402 | _userHidOptions = false; |
403 | _updateActions(); |
404 | _updateOverlay(); |
405 | return; |
406 | } |
407 | _updateHighlight(_highlightedOptionIndex.value + 1); |
408 | } |
409 | |
410 | Object? _hideOptions(DismissIntent intent) { |
411 | if (!_userHidOptions) { |
412 | _userHidOptions = true; |
413 | _updateActions(); |
414 | _updateOverlay(); |
415 | return null; |
416 | } |
417 | return Actions.invoke(context, intent); |
418 | } |
419 | |
420 | void _setActionsEnabled(bool enabled) { |
421 | // The enabled state determines whether the action will consume the |
422 | // key shortcut or let it continue on to the underlying text field. |
423 | // They should only be enabled when the options are showing so shortcuts |
424 | // can be used to navigate them. |
425 | _previousOptionAction.enabled = enabled; |
426 | _nextOptionAction.enabled = enabled; |
427 | _hideOptionsAction.enabled = enabled; |
428 | } |
429 | |
430 | void _updateActions() { |
431 | _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty); |
432 | } |
433 | |
434 | bool _floatingOptionsUpdateScheduled = false; |
435 | // Hide or show the options overlay, if needed. |
436 | void _updateOverlay() { |
437 | if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { |
438 | if (!_floatingOptionsUpdateScheduled) { |
439 | _floatingOptionsUpdateScheduled = true; |
440 | SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { |
441 | _floatingOptionsUpdateScheduled = false; |
442 | _updateOverlay(); |
443 | }, debugLabel: 'RawAutoComplete.updateOverlay' ); |
444 | } |
445 | return; |
446 | } |
447 | |
448 | _floatingOptions?.remove(); |
449 | _floatingOptions?.dispose(); |
450 | if (_shouldShowOptions) { |
451 | final OverlayEntry newFloatingOptions = OverlayEntry( |
452 | builder: (BuildContext context) { |
453 | return CompositedTransformFollower( |
454 | link: _optionsLayerLink, |
455 | showWhenUnlinked: false, |
456 | targetAnchor: switch (widget.optionsViewOpenDirection) { |
457 | OptionsViewOpenDirection.up => Alignment.topLeft, |
458 | OptionsViewOpenDirection.down => Alignment.bottomLeft, |
459 | }, |
460 | followerAnchor: switch (widget.optionsViewOpenDirection) { |
461 | OptionsViewOpenDirection.up => Alignment.bottomLeft, |
462 | OptionsViewOpenDirection.down => Alignment.topLeft, |
463 | }, |
464 | child: TextFieldTapRegion( |
465 | child: AutocompleteHighlightedOption( |
466 | highlightIndexNotifier: _highlightedOptionIndex, |
467 | child: Builder( |
468 | builder: (BuildContext context) { |
469 | return widget.optionsViewBuilder(context, _select, _options); |
470 | } |
471 | ) |
472 | ), |
473 | ), |
474 | ); |
475 | }, |
476 | ); |
477 | Overlay.of(context, rootOverlay: true, debugRequiredFor: widget).insert(newFloatingOptions); |
478 | _floatingOptions = newFloatingOptions; |
479 | } else { |
480 | _floatingOptions = null; |
481 | } |
482 | } |
483 | |
484 | // Handle a potential change in textEditingController by properly disposing of |
485 | // the old one and setting up the new one, if needed. |
486 | void _updateTextEditingController(TextEditingController? old, TextEditingController? current) { |
487 | if ((old == null && current == null) || old == current) { |
488 | return; |
489 | } |
490 | if (old == null) { |
491 | _textEditingController.removeListener(_onChangedField); |
492 | _textEditingController.dispose(); |
493 | _textEditingController = current!; |
494 | } else if (current == null) { |
495 | _textEditingController.removeListener(_onChangedField); |
496 | _textEditingController = TextEditingController(); |
497 | } else { |
498 | _textEditingController.removeListener(_onChangedField); |
499 | _textEditingController = current; |
500 | } |
501 | _textEditingController.addListener(_onChangedField); |
502 | } |
503 | |
504 | // Handle a potential change in focusNode by properly disposing of the old one |
505 | // and setting up the new one, if needed. |
506 | void _updateFocusNode(FocusNode? old, FocusNode? current) { |
507 | if ((old == null && current == null) || old == current) { |
508 | return; |
509 | } |
510 | if (old == null) { |
511 | _focusNode.removeListener(_onChangedFocus); |
512 | _focusNode.dispose(); |
513 | _focusNode = current!; |
514 | } else if (current == null) { |
515 | _focusNode.removeListener(_onChangedFocus); |
516 | _focusNode = FocusNode(); |
517 | } else { |
518 | _focusNode.removeListener(_onChangedFocus); |
519 | _focusNode = current; |
520 | } |
521 | _focusNode.addListener(_onChangedFocus); |
522 | } |
523 | |
524 | @override |
525 | void initState() { |
526 | super.initState(); |
527 | _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue); |
528 | _textEditingController.addListener(_onChangedField); |
529 | _focusNode = widget.focusNode ?? FocusNode(); |
530 | _focusNode.addListener(_onChangedFocus); |
531 | _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption); |
532 | _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption); |
533 | _hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions); |
534 | _actionMap = <Type, Action<Intent>> { |
535 | AutocompletePreviousOptionIntent: _previousOptionAction, |
536 | AutocompleteNextOptionIntent: _nextOptionAction, |
537 | DismissIntent: _hideOptionsAction, |
538 | }; |
539 | _updateActions(); |
540 | _updateOverlay(); |
541 | } |
542 | |
543 | @override |
544 | void didUpdateWidget(RawAutocomplete<T> oldWidget) { |
545 | super.didUpdateWidget(oldWidget); |
546 | _updateTextEditingController( |
547 | oldWidget.textEditingController, |
548 | widget.textEditingController, |
549 | ); |
550 | _updateFocusNode(oldWidget.focusNode, widget.focusNode); |
551 | _updateActions(); |
552 | _updateOverlay(); |
553 | } |
554 | |
555 | @override |
556 | void dispose() { |
557 | _textEditingController.removeListener(_onChangedField); |
558 | if (widget.textEditingController == null) { |
559 | _textEditingController.dispose(); |
560 | } |
561 | _focusNode.removeListener(_onChangedFocus); |
562 | if (widget.focusNode == null) { |
563 | _focusNode.dispose(); |
564 | } |
565 | _floatingOptions?.remove(); |
566 | _floatingOptions?.dispose(); |
567 | _floatingOptions = null; |
568 | _highlightedOptionIndex.dispose(); |
569 | super.dispose(); |
570 | } |
571 | |
572 | @override |
573 | Widget build(BuildContext context) { |
574 | return TextFieldTapRegion( |
575 | child: Container( |
576 | key: _fieldKey, |
577 | child: Shortcuts( |
578 | shortcuts: _shortcuts, |
579 | child: Actions( |
580 | actions: _actionMap, |
581 | child: CompositedTransformTarget( |
582 | link: _optionsLayerLink, |
583 | child: widget.fieldViewBuilder == null |
584 | ? const SizedBox.shrink() |
585 | : widget.fieldViewBuilder!( |
586 | context, |
587 | _textEditingController, |
588 | _focusNode, |
589 | _onFieldSubmitted, |
590 | ), |
591 | ), |
592 | ), |
593 | ), |
594 | ), |
595 | ); |
596 | } |
597 | } |
598 | |
599 | class _AutocompleteCallbackAction<T extends Intent> extends CallbackAction<T> { |
600 | _AutocompleteCallbackAction({ |
601 | required super.onInvoke, |
602 | this.enabled = true, |
603 | }); |
604 | |
605 | bool enabled; |
606 | |
607 | @override |
608 | bool isEnabled(covariant T intent) => enabled; |
609 | |
610 | @override |
611 | bool consumesKey(covariant T intent) => enabled; |
612 | } |
613 | |
614 | /// An [Intent] to highlight the previous option in the autocomplete list. |
615 | class AutocompletePreviousOptionIntent extends Intent { |
616 | /// Creates an instance of AutocompletePreviousOptionIntent. |
617 | const AutocompletePreviousOptionIntent(); |
618 | } |
619 | |
620 | /// An [Intent] to highlight the next option in the autocomplete list. |
621 | class AutocompleteNextOptionIntent extends Intent { |
622 | /// Creates an instance of AutocompleteNextOptionIntent. |
623 | const AutocompleteNextOptionIntent(); |
624 | } |
625 | |
626 | /// An inherited widget used to indicate which autocomplete option should be |
627 | /// highlighted for keyboard navigation. |
628 | /// |
629 | /// The `RawAutoComplete` widget will wrap the options view generated by the |
630 | /// `optionsViewBuilder` with this widget to provide the highlighted option's |
631 | /// index to the builder. |
632 | /// |
633 | /// In the builder callback the index of the highlighted option can be obtained |
634 | /// by using the static [of] method: |
635 | /// |
636 | /// ```dart |
637 | /// int highlightedIndex = AutocompleteHighlightedOption.of(context); |
638 | /// ``` |
639 | /// |
640 | /// which can then be used to tell which option should be given a visual |
641 | /// indication that will be the option selected with the keyboard. |
642 | class AutocompleteHighlightedOption extends InheritedNotifier<ValueNotifier<int>> { |
643 | /// Create an instance of AutocompleteHighlightedOption inherited widget. |
644 | const AutocompleteHighlightedOption({ |
645 | super.key, |
646 | required ValueNotifier<int> highlightIndexNotifier, |
647 | required super.child, |
648 | }) : super(notifier: highlightIndexNotifier); |
649 | |
650 | /// Returns the index of the highlighted option from the closest |
651 | /// [AutocompleteHighlightedOption] ancestor. |
652 | /// |
653 | /// If there is no ancestor, it returns 0. |
654 | /// |
655 | /// Typical usage is as follows: |
656 | /// |
657 | /// ```dart |
658 | /// int highlightedIndex = AutocompleteHighlightedOption.of(context); |
659 | /// ``` |
660 | static int of(BuildContext context) { |
661 | return context.dependOnInheritedWidgetOfExactType<AutocompleteHighlightedOption>()?.notifier?.value ?? 0; |
662 | } |
663 | } |
664 | |