diff --git a/app/lib/presentation/widgets/autocomplete.dart b/app/lib/presentation/widgets/autocomplete.dart deleted file mode 100644 index 9d7a5aca..00000000 --- a/app/lib/presentation/widgets/autocomplete.dart +++ /dev/null @@ -1,438 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class _AutocompleteCallbackAction extends CallbackAction { - _AutocompleteCallbackAction({ - required super.onInvoke, - required this.isEnabledCallback, - }); - - // The enabled state determines whether the action will consume the - // key shortcut or let it continue on to the underlying text field. - // They should only be enabled when the options are showing so shortcuts - // can be used to navigate them. - final bool Function() isEnabledCallback; - - @override - bool isEnabled(covariant T intent) => isEnabledCallback(); - - @override - bool consumesKey(covariant T intent) => isEnabled(intent); -} - -class ExternalResultsAutocomplete extends StatefulWidget { - /// Create an instance of RawAutocomplete. - /// - /// [displayStringForOption], [onTextChanged] and [optionsViewBuilder] must - /// not be null. - const ExternalResultsAutocomplete({ - super.key, - required this.optionsViewBuilder, - required this.onTextChanged, - required this.optionsStream, - this.optionsViewOpenDirection = OptionsViewOpenDirection.down, - this.displayStringForOption = defaultStringForOption, - this.fieldViewBuilder, - this.focusNode, - this.onSelected, - this.textEditingController, - this.initialValue, - }) : assert( - fieldViewBuilder != null || - (key != null && - focusNode != null && - textEditingController != null), - '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.', - ), - assert((focusNode == null) == (textEditingController == null)), - assert( - !(textEditingController != null && initialValue != null), - 'textEditingController and initialValue cannot be simultaneously defined.', - ); - - /// {@template flutter.widgets.RawAutocomplete.fieldViewBuilder} - /// Builds the field whose input is used to get the options. - /// - /// Pass the provided [TextEditingController] to the field built here so that - /// RawAutocomplete can listen for changes. - /// {@endtemplate} - /// - /// If this parameter is null, then a [SizedBox.shrink] is built instead. - /// For how that pattern can be useful, see [textEditingController]. - final AutocompleteFieldViewBuilder? fieldViewBuilder; - - /// The [FocusNode] that is used for the text field. - /// - /// {@template flutter.widgets.RawAutocomplete.split} - /// The main purpose of this parameter is to allow the use of a separate text - /// field located in another part of the widget tree instead of the text - /// field built by [fieldViewBuilder]. For example, it may be desirable to - /// place the text field in the AppBar and the options below in the main body. - /// - /// When following this pattern, [fieldViewBuilder] can be omitted, - /// so that a text field is not drawn where it would normally be. - /// A separate text field can be created elsewhere, and a - /// FocusNode and TextEditingController can be passed both to that text field - /// and to RawAutocomplete. - /// - /// {@tool dartpad} - /// This examples shows how to create an autocomplete widget with the text - /// field in the AppBar and the results in the main body of the app. - /// - /// ** See code in examples/api/lib/widgets/autocomplete/raw_autocomplete.focus_node.0.dart ** - /// {@end-tool} - /// {@endtemplate} - /// - /// If this parameter is not null, then [textEditingController] must also be - /// not null. - final FocusNode? focusNode; - - /// {@template flutter.widgets.RawAutocomplete.optionsViewBuilder} - /// Builds the selectable options widgets from a list of options objects. - /// - /// The options are displayed floating below or above the field using a - /// [CompositedTransformFollower] inside of an [Overlay], not at the same - /// place in the widget tree as [ExternalResultsAutocomplete]. To control whether it opens - /// upward or downward, use [optionsViewOpenDirection]. - /// - /// In order to track which item is highlighted by keyboard navigation, the - /// resulting options will be wrapped in an inherited - /// [AutocompleteHighlightedOption] widget. - /// Inside this callback, the index of the highlighted option can be obtained - /// from [AutocompleteHighlightedOption.of] to display the highlighted option - /// with a visual highlight to indicate it will be the option selected from - /// the keyboard. - /// - /// {@endtemplate} - final AutocompleteOptionsViewBuilder optionsViewBuilder; - - /// {@template flutter.widgets.RawAutocomplete.optionsViewOpenDirection} - /// The direction in which to open the options-view overlay. - /// - /// Defaults to [OptionsViewOpenDirection.down]. - /// {@endtemplate} - final OptionsViewOpenDirection optionsViewOpenDirection; - - /// {@template flutter.widgets.RawAutocomplete.displayStringForOption} - /// Returns the string to display in the field when the option is selected. - /// - /// This is useful when using a custom T type and the string to display is - /// different than the string to search by. - /// - /// If not provided, will use `option.toString()`. - /// {@endtemplate} - final AutocompleteOptionToString displayStringForOption; - - /// {@template flutter.widgets.RawAutocomplete.onSelected} - /// Called when an option is selected by the user. - /// {@endtemplate} - final AutocompleteOnSelected? onSelected; - - // ignore: avoid_futureor_void - final FutureOr Function(TextEditingValue textEditingValue) - onTextChanged; - - /// The [TextEditingController] that is used for the text field. - /// - /// {@macro flutter.widgets.RawAutocomplete.split} - /// - /// If this parameter is not null, then [focusNode] must also be not null. - final TextEditingController? textEditingController; - - /// {@template flutter.widgets.RawAutocomplete.initialValue} - /// The initial value to use for the text field. - /// {@endtemplate} - /// - /// Setting the initial value does not notify [textEditingController]'s - /// listeners, and thus will not cause the options UI to appear. - /// - /// This parameter is ignored if [textEditingController] is defined. - final TextEditingValue? initialValue; - - final Stream> optionsStream; - - /// Calls [AutocompleteFieldViewBuilder]'s onFieldSubmitted callback for the - /// RawAutocomplete widget indicated by the given [GlobalKey]. - /// - /// This is not typically used unless a custom field is implemented instead of - /// using [fieldViewBuilder]. In the typical case, the onFieldSubmitted - /// callback is passed via the [AutocompleteFieldViewBuilder] signature. When - /// not using fieldViewBuilder, the same callback can be called by using this - /// static method. - /// - /// See also: - /// - /// * [focusNode] and [textEditingController], which contain a code example - /// showing how to create a separate field outside of fieldViewBuilder. - static void onFieldSubmitted(GlobalKey key) { - final _RawAutocompleteState rawAutocomplete = - key.currentState! as _RawAutocompleteState; - rawAutocomplete._onFieldSubmitted(); - } - - /// The default way to convert an option to a string in - /// [displayStringForOption]. - /// - /// Uses the `toString` method of the given `option`. - static String defaultStringForOption(Object? option) { - return option.toString(); - } - - @override - State> createState() => - _RawAutocompleteState(); -} - -class _RawAutocompleteState - extends State> { - final GlobalKey _fieldKey = GlobalKey(); - final LayerLink _optionsLayerLink = LayerLink(); - final OverlayPortalController _optionsViewController = - OverlayPortalController(debugLabel: '_RawAutocompleteState'); - - TextEditingController? _internalTextEditingController; - TextEditingController get _textEditingController { - return widget.textEditingController ?? - (_internalTextEditingController ??= - TextEditingController()..addListener(_onChangedField)); - } - - FocusNode? _internalFocusNode; - FocusNode get _focusNode { - return widget.focusNode ?? - (_internalFocusNode ??= - FocusNode()..addListener(_updateOptionsViewVisibility)); - } - - late final Map> _actionMap = - >{ - AutocompletePreviousOptionIntent: - _AutocompleteCallbackAction( - onInvoke: _highlightPreviousOption, - isEnabledCallback: () => _canShowOptionsView, - ), - AutocompleteNextOptionIntent: - _AutocompleteCallbackAction( - onInvoke: _highlightNextOption, - isEnabledCallback: () => _canShowOptionsView, - ), - DismissIntent: CallbackAction(onInvoke: _hideOptions), - }; - - late StreamSubscription> _optionsSubscription; - Iterable _options = Iterable.empty(); - T? _selection; - // Set the initial value to null so when this widget gets focused for the first - // time it will try to run the options view builder. - String? _lastFieldText; - final ValueNotifier _highlightedOptionIndex = ValueNotifier(0); - - static const Map _shortcuts = - { - SingleActivator(LogicalKeyboardKey.arrowUp): - AutocompletePreviousOptionIntent(), - SingleActivator(LogicalKeyboardKey.arrowDown): - AutocompleteNextOptionIntent(), - }; - - bool get _canShowOptionsView => - _focusNode.hasFocus && _selection == null && _options.isNotEmpty; - - void _updateOptionsViewVisibility() { - if (_canShowOptionsView) { - _optionsViewController.show(); - } else { - _optionsViewController.hide(); - } - } - - void _onUpateOptions(Iterable options) { - final TextEditingValue value = _textEditingController.value; - - _options = options; - _updateHighlight(_highlightedOptionIndex.value); - final T? selection = _selection; - if (selection != null && - value.text != widget.displayStringForOption(selection)) { - _selection = null; - } - - // Make sure the options are no longer hidden if the content of the field - // changes (ignore selection changes). - if (value.text != _lastFieldText) { - _lastFieldText = value.text; - _updateOptionsViewVisibility(); - } - } - - // Called when _textEditingController changes. - Future _onChangedField() async { - final TextEditingValue value = _textEditingController.value; - await widget.onTextChanged(value); - } - - // Called from fieldViewBuilder when the user submits the field. - void _onFieldSubmitted() { - if (_optionsViewController.isShowing) { - _select(_options.elementAt(_highlightedOptionIndex.value)); - } - } - - // Select the given option and update the widget. - void _select(T nextSelection) { - if (nextSelection == _selection) { - return; - } - _selection = nextSelection; - final String selectionString = widget.displayStringForOption(nextSelection); - _textEditingController.value = TextEditingValue( - selection: TextSelection.collapsed(offset: selectionString.length), - text: selectionString, - ); - widget.onSelected?.call(nextSelection); - _updateOptionsViewVisibility(); - } - - void _updateHighlight(int newIndex) { - _highlightedOptionIndex.value = - _options.isEmpty ? 0 : newIndex % _options.length; - } - - void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) { - assert(_canShowOptionsView); - _updateOptionsViewVisibility(); - assert(_optionsViewController.isShowing); - _updateHighlight(_highlightedOptionIndex.value - 1); - } - - void _highlightNextOption(AutocompleteNextOptionIntent intent) { - assert(_canShowOptionsView); - _updateOptionsViewVisibility(); - assert(_optionsViewController.isShowing); - _updateHighlight(_highlightedOptionIndex.value + 1); - } - - Object? _hideOptions(DismissIntent intent) { - if (_optionsViewController.isShowing) { - _optionsViewController.hide(); - return null; - } else { - return Actions.invoke(context, intent); - } - } - - Widget _buildOptionsView(BuildContext context) { - final TextDirection textDirection = Directionality.of(context); - final Alignment followerAlignment = switch (widget - .optionsViewOpenDirection) { - OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, - OptionsViewOpenDirection.down => AlignmentDirectional.topStart, - }.resolve(textDirection); - final Alignment targetAnchor = switch (widget.optionsViewOpenDirection) { - OptionsViewOpenDirection.up => AlignmentDirectional.topStart, - OptionsViewOpenDirection.down => AlignmentDirectional.bottomStart, - }.resolve(textDirection); - - return CompositedTransformFollower( - link: _optionsLayerLink, - showWhenUnlinked: false, - targetAnchor: targetAnchor, - followerAnchor: followerAlignment, - child: TextFieldTapRegion( - child: AutocompleteHighlightedOption( - highlightIndexNotifier: _highlightedOptionIndex, - child: Builder( - builder: - (BuildContext context) => - widget.optionsViewBuilder(context, _select, _options), - ), - ), - ), - ); - } - - @override - void initState() { - super.initState(); - final TextEditingController initialController = - widget.textEditingController ?? - (_internalTextEditingController = TextEditingController.fromValue( - widget.initialValue, - )); - initialController.addListener(_onChangedField); - widget.focusNode?.addListener(_updateOptionsViewVisibility); - _optionsSubscription = widget.optionsStream.listen(_onUpateOptions); - } - - @override - void didUpdateWidget(ExternalResultsAutocomplete oldWidget) { - super.didUpdateWidget(oldWidget); - if (!identical( - oldWidget.textEditingController, - widget.textEditingController, - )) { - oldWidget.textEditingController?.removeListener(_onChangedField); - if (oldWidget.textEditingController == null) { - _internalTextEditingController?.dispose(); - _internalTextEditingController = null; - } - widget.textEditingController?.addListener(_onChangedField); - } - if (!identical(oldWidget.focusNode, widget.focusNode)) { - oldWidget.focusNode?.removeListener(_updateOptionsViewVisibility); - if (oldWidget.focusNode == null) { - _internalFocusNode?.dispose(); - _internalFocusNode = null; - } - widget.focusNode?.addListener(_updateOptionsViewVisibility); - } - if (!identical(oldWidget.optionsStream, widget.optionsStream)) { - unawaited(_optionsSubscription.cancel()); - _optionsSubscription = widget.optionsStream.listen(_onUpateOptions); - } - } - - @override - void dispose() { - widget.textEditingController?.removeListener(_onChangedField); - _internalTextEditingController?.dispose(); - widget.focusNode?.removeListener(_updateOptionsViewVisibility); - _internalFocusNode?.dispose(); - _highlightedOptionIndex.dispose(); - unawaited(_optionsSubscription.cancel()); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Widget fieldView = - widget.fieldViewBuilder?.call( - context, - _textEditingController, - _focusNode, - _onFieldSubmitted, - ) ?? - const SizedBox.shrink(); - return OverlayPortal.targetsRootOverlay( - controller: _optionsViewController, - overlayChildBuilder: _buildOptionsView, - child: TextFieldTapRegion( - child: Container( - key: _fieldKey, - child: Shortcuts( - shortcuts: _shortcuts, - child: Actions( - actions: _actionMap, - child: CompositedTransformTarget( - link: _optionsLayerLink, - child: fieldView, - ), - ), - ), - ), - ), - ); - } -}