diff --git a/packages/neon/lib/src/apps/notes/app.dart b/packages/neon/lib/src/apps/notes/app.dart index cb50d3dc..2a82b692 100644 --- a/packages/neon/lib/src/apps/notes/app.dart +++ b/packages/neon/lib/src/apps/notes/app.dart @@ -10,7 +10,6 @@ import 'package:neon/l10n/localizations.dart'; import 'package:neon/src/apps/notes/blocs/notes.dart'; import 'package:neon/src/blocs/apps.dart'; import 'package:neon/src/neon.dart'; -import 'package:neon/src/widgets/custom_auto_complete.dart'; import 'package:nextcloud/nextcloud.dart'; import 'package:provider/provider.dart'; import 'package:rxdart/rxdart.dart'; diff --git a/packages/neon/lib/src/apps/notes/widgets/category_select.dart b/packages/neon/lib/src/apps/notes/widgets/category_select.dart index f90e274c..5a69ed92 100644 --- a/packages/neon/lib/src/apps/notes/widgets/category_select.dart +++ b/packages/neon/lib/src/apps/notes/widgets/category_select.dart @@ -21,7 +21,7 @@ class NotesCategorySelect extends StatelessWidget { late final _categories = categories..sort((final a, final b) => a.compareTo(b)); @override - Widget build(final BuildContext context) => CustomAutocomplete( + Widget build(final BuildContext context) => Autocomplete( initialValue: initialValue != null ? TextEditingValue( text: initialValue!, @@ -59,19 +59,38 @@ class NotesCategorySelect extends StatelessWidget { }, onChanged: onChanged, ), - displayWidgetForOption: (final category) => Row( - children: [ - Icon( - MdiIcons.tag, - color: category != '' ? NotesCategoryColor.compute(category) : null, + optionsViewBuilder: (final context, final onSelected, final options) => Align( + alignment: Alignment.topLeft, + child: Material( + elevation: 4, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 200), + child: ListView.builder( + padding: EdgeInsets.zero, + shrinkWrap: true, + itemCount: options.length, + itemBuilder: (final context, final index) { + final option = options.elementAt(index); + return InkWell( + onTap: () { + onSelected(option); + }, + child: Builder( + builder: (final context) => ListTile( + leading: Icon( + MdiIcons.tag, + color: option != '' ? NotesCategoryColor.compute(option) : null, + ), + title: Text( + option != '' ? option : AppLocalizations.of(context).notesUncategorized, + ), + ), + ), + ); + }, + ), ), - const SizedBox( - width: 10, - ), - Text( - category != '' ? category : AppLocalizations.of(context).notesUncategorized, - ), - ], + ), ), onSelected: (final value) { if (categories.contains(value)) { diff --git a/packages/neon/lib/src/widgets/custom_auto_complete.dart b/packages/neon/lib/src/widgets/custom_auto_complete.dart deleted file mode 100644 index 1da79c23..00000000 --- a/packages/neon/lib/src/widgets/custom_auto_complete.dart +++ /dev/null @@ -1,493 +0,0 @@ -// 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 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; - -class CustomAutocomplete extends StatelessWidget { - const CustomAutocomplete({ - required this.optionsBuilder, - this.displayWidgetForOption = CustomRawAutocomplete.defaultWidgetForOption, - this.fieldViewBuilder = _defaultFieldViewBuilder, - this.onSelected, - this.optionsMaxHeight = 200.0, - this.optionsViewBuilder, - this.initialValue, - super.key, - }); - - final AutocompleteOptionToWidget displayWidgetForOption; - - final AutocompleteFieldViewBuilder fieldViewBuilder; - - final AutocompleteOnSelected? onSelected; - - final AutocompleteOptionsBuilder optionsBuilder; - - final AutocompleteOptionsViewBuilder? optionsViewBuilder; - - final double optionsMaxHeight; - - final TextEditingValue? initialValue; - - static Widget _defaultFieldViewBuilder( - final BuildContext context, - final TextEditingController textEditingController, - final FocusNode focusNode, - final VoidCallback onFieldSubmitted, - ) => - _CustomAutocompleteField( - focusNode: focusNode, - textEditingController: textEditingController, - onFieldSubmitted: onFieldSubmitted, - ); - - @override - Widget build(final BuildContext context) => CustomRawAutocomplete( - displayWidgetForOption: displayWidgetForOption, - fieldViewBuilder: fieldViewBuilder, - initialValue: initialValue, - optionsBuilder: optionsBuilder, - optionsViewBuilder: optionsViewBuilder ?? - ( - final context, - final onSelected, - final options, - ) => - _CustomAutocompleteOptions( - displayWidgetForOption: displayWidgetForOption, - onSelected: onSelected, - options: options, - maxOptionsHeight: optionsMaxHeight, - ), - onSelected: onSelected, - ); -} - -// The default Material-style Autocomplete text field. -class _CustomAutocompleteField extends StatelessWidget { - const _CustomAutocompleteField({ - required this.focusNode, - required this.textEditingController, - required this.onFieldSubmitted, - }); - - final FocusNode focusNode; - - final VoidCallback onFieldSubmitted; - - final TextEditingController textEditingController; - - @override - Widget build(final BuildContext context) => TextFormField( - controller: textEditingController, - focusNode: focusNode, - onFieldSubmitted: (final value) { - onFieldSubmitted(); - }, - ); -} - -// The default Material-style Autocomplete options. -class _CustomAutocompleteOptions extends StatelessWidget { - const _CustomAutocompleteOptions({ - required this.displayWidgetForOption, - required this.onSelected, - required this.options, - required this.maxOptionsHeight, - super.key, - }); - - final AutocompleteOptionToWidget displayWidgetForOption; - - final AutocompleteOnSelected onSelected; - - final Iterable options; - final double maxOptionsHeight; - - @override - Widget build(final BuildContext context) => Align( - alignment: Alignment.topLeft, - child: Material( - elevation: 4, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxOptionsHeight), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - itemCount: options.length, - itemBuilder: (final context, final index) { - final option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Builder( - builder: (final context) { - final highlight = AutocompleteHighlightedOption.of(context) == index; - if (highlight) { - SchedulerBinding.instance.addPostFrameCallback((final timeStamp) async { - await Scrollable.ensureVisible(context, alignment: 0.5); - }); - } - return Container( - color: highlight ? Theme.of(context).focusColor : null, - padding: const EdgeInsets.all(16), - child: displayWidgetForOption(option), - ); - }, - ), - ); - }, - ), - ), - ), - ); -} - -typedef AutocompleteOptionToWidget = Widget Function(T option); - -class CustomRawAutocomplete extends StatefulWidget { - const CustomRawAutocomplete({ - required this.optionsViewBuilder, - required this.optionsBuilder, - this.displayWidgetForOption = defaultWidgetForOption, - this.fieldViewBuilder, - this.focusNode, - this.onSelected, - this.textEditingController, - this.initialValue, - super.key, - }) : 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.', - ), - // ignore: prefer_asserts_with_message - assert((focusNode == null) == (textEditingController == null)), - assert( - !(textEditingController != null && initialValue != null), - 'textEditingController and initialValue cannot be simultaneously defined.', - ); - - final AutocompleteFieldViewBuilder? fieldViewBuilder; - - final FocusNode? focusNode; - - final AutocompleteOptionsViewBuilder optionsViewBuilder; - - final AutocompleteOptionToWidget displayWidgetForOption; - - final AutocompleteOnSelected? onSelected; - - final AutocompleteOptionsBuilder optionsBuilder; - - final TextEditingController? textEditingController; - - final TextEditingValue? initialValue; - - static void onFieldSubmitted(final GlobalKey key) { - (key.currentState! as _CustomRawAutocompleteState)._onFieldSubmitted(); - } - - static Widget defaultWidgetForOption(final dynamic option) => Text(option.toString()); - - @override - State> createState() => _CustomRawAutocompleteState(); -} - -class _CustomRawAutocompleteState extends State> { - final GlobalKey _fieldKey = GlobalKey(); - final LayerLink _optionsLayerLink = LayerLink(); - late TextEditingController _textEditingController; - late FocusNode _focusNode; - late final Map> _actionMap; - late final _CustomAutocompleteCallbackAction _previousOptionAction; - late final _CustomAutocompleteCallbackAction _nextOptionAction; - late final _CustomAutocompleteCallbackAction _hideOptionsAction; - Iterable _options = Iterable.empty(); - T? _selection; - bool _userHidOptions = false; - String _lastFieldText = ''; - final ValueNotifier _highlightedOptionIndex = ValueNotifier(0); - - static const Map _shortcuts = { - SingleActivator(LogicalKeyboardKey.arrowUp): AutocompletePreviousOptionIntent(), - SingleActivator(LogicalKeyboardKey.arrowDown): AutocompleteNextOptionIntent(), - }; - - // The OverlayEntry containing the options. - OverlayEntry? _floatingOptions; - - // True iff the state indicates that the options should be visible. - bool get _shouldShowOptions => !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty; - - // Called when _textEditingController changes. - Future _onChangedField() async { - final value = _textEditingController.value; - final options = await widget.optionsBuilder( - value, - ); - _options = options; - _updateHighlight(_highlightedOptionIndex.value); - if (_selection != null && value.text != _selection!.toString()) { - _selection = null; - } - - // Make sure the options are no longer hidden if the content of the field - // changes (ignore selection changes). - if (value.text != _lastFieldText) { - _userHidOptions = false; - _lastFieldText = value.text; - } - _updateActions(); - _updateOverlay(); - } - - // Called when the field's FocusNode changes. - void _onChangedFocus() { - // Options should no longer be hidden when the field is re-focused. - _userHidOptions = !_focusNode.hasFocus; - _updateActions(); - _updateOverlay(); - } - - // Called from fieldViewBuilder when the user submits the field. - void _onFieldSubmitted() { - if (_options.isEmpty || _userHidOptions) { - return; - } - _select(_options.elementAt(_highlightedOptionIndex.value)); - } - - // Select the given option and update the widget. - void _select(final T nextSelection) { - if (nextSelection == _selection) { - return; - } - _selection = nextSelection; - final selectionString = nextSelection.toString(); - _textEditingController.value = TextEditingValue( - selection: TextSelection.collapsed(offset: selectionString.length), - text: selectionString, - ); - _updateActions(); - _updateOverlay(); - widget.onSelected?.call(_selection!); - } - - void _updateHighlight(final int newIndex) { - _highlightedOptionIndex.value = _options.isEmpty ? 0 : newIndex % _options.length; - } - - void _highlightPreviousOption(final AutocompletePreviousOptionIntent intent) { - if (_userHidOptions) { - _userHidOptions = false; - _updateActions(); - _updateOverlay(); - return; - } - _updateHighlight(_highlightedOptionIndex.value - 1); - } - - void _highlightNextOption(final AutocompleteNextOptionIntent intent) { - if (_userHidOptions) { - _userHidOptions = false; - _updateActions(); - _updateOverlay(); - return; - } - _updateHighlight(_highlightedOptionIndex.value + 1); - } - - Object? _hideOptions(final DismissIntent intent) { - if (!_userHidOptions) { - _userHidOptions = true; - _updateActions(); - _updateOverlay(); - return null; - } - return Actions.invoke(context, intent); - } - - void _setActionsEnabled(final bool enabled) { - // 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. - _previousOptionAction.enabled = enabled; - _nextOptionAction.enabled = enabled; - _hideOptionsAction.enabled = enabled; - } - - void _updateActions() { - _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty); - } - - bool _floatingOptionsUpdateScheduled = false; - // Hide or show the options overlay, if needed. - void _updateOverlay() { - if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { - if (!_floatingOptionsUpdateScheduled) { - _floatingOptionsUpdateScheduled = true; - SchedulerBinding.instance.addPostFrameCallback((final timeStamp) { - _floatingOptionsUpdateScheduled = false; - _updateOverlay(); - }); - } - return; - } - - _floatingOptions?.remove(); - if (_shouldShowOptions) { - final newFloatingOptions = OverlayEntry( - builder: (final context) => CompositedTransformFollower( - link: _optionsLayerLink, - showWhenUnlinked: false, - targetAnchor: Alignment.bottomLeft, - child: AutocompleteHighlightedOption( - highlightIndexNotifier: _highlightedOptionIndex, - child: Builder( - builder: (final context) => widget.optionsViewBuilder( - context, - _select, - _options, - ), - ), - ), - ), - ); - Overlay.of(context, rootOverlay: true)!.insert(newFloatingOptions); - _floatingOptions = newFloatingOptions; - } else { - _floatingOptions = null; - } - } - - // Handle a potential change in textEditingController by properly disposing of - // the old one and setting up the new one, if needed. - void _updateTextEditingController(final TextEditingController? old, final TextEditingController? current) { - if ((old == null && current == null) || old == current) { - return; - } - if (old == null) { - _textEditingController - ..removeListener(_onChangedField) - ..dispose(); - _textEditingController = current!; - } else if (current == null) { - _textEditingController.removeListener(_onChangedField); - _textEditingController = TextEditingController(); - } else { - _textEditingController.removeListener(_onChangedField); - _textEditingController = current; - } - _textEditingController.addListener(_onChangedField); - } - - // Handle a potential change in focusNode by properly disposing of the old one - // and setting up the new one, if needed. - void _updateFocusNode(final FocusNode? old, final FocusNode? current) { - if ((old == null && current == null) || old == current) { - return; - } - if (old == null) { - _focusNode - ..removeListener(_onChangedFocus) - ..dispose(); - _focusNode = current!; - } else if (current == null) { - _focusNode.removeListener(_onChangedFocus); - _focusNode = FocusNode(); - } else { - _focusNode.removeListener(_onChangedFocus); - _focusNode = current; - } - _focusNode.addListener(_onChangedFocus); - } - - @override - void initState() { - super.initState(); - _textEditingController = widget.textEditingController ?? TextEditingController.fromValue(widget.initialValue); - _textEditingController.addListener(_onChangedField); - _focusNode = widget.focusNode ?? FocusNode(); - _focusNode.addListener(_onChangedFocus); - _previousOptionAction = - _CustomAutocompleteCallbackAction(onInvoke: _highlightPreviousOption); - _nextOptionAction = _CustomAutocompleteCallbackAction(onInvoke: _highlightNextOption); - _hideOptionsAction = _CustomAutocompleteCallbackAction(onInvoke: _hideOptions); - _actionMap = >{ - AutocompletePreviousOptionIntent: _previousOptionAction, - AutocompleteNextOptionIntent: _nextOptionAction, - DismissIntent: _hideOptionsAction, - }; - _updateActions(); - _updateOverlay(); - } - - @override - void didUpdateWidget(final CustomRawAutocomplete oldWidget) { - super.didUpdateWidget(oldWidget); - _updateTextEditingController( - oldWidget.textEditingController, - widget.textEditingController, - ); - _updateFocusNode(oldWidget.focusNode, widget.focusNode); - _updateActions(); - _updateOverlay(); - } - - @override - void dispose() { - _textEditingController.removeListener(_onChangedField); - if (widget.textEditingController == null) { - _textEditingController.dispose(); - } - _focusNode.removeListener(_onChangedFocus); - if (widget.focusNode == null) { - _focusNode.dispose(); - } - _floatingOptions?.remove(); - _floatingOptions = null; - super.dispose(); - } - - @override - Widget build(final BuildContext context) => Container( - key: _fieldKey, - child: Shortcuts( - shortcuts: _shortcuts, - child: Actions( - actions: _actionMap, - child: CompositedTransformTarget( - link: _optionsLayerLink, - child: widget.fieldViewBuilder == null - ? const SizedBox.shrink() - : widget.fieldViewBuilder!( - context, - _textEditingController, - _focusNode, - _onFieldSubmitted, - ), - ), - ), - ), - ); -} - -class _CustomAutocompleteCallbackAction extends CallbackAction { - _CustomAutocompleteCallbackAction({ - required super.onInvoke, - this.enabled = true, - }); - - bool enabled; - - @override - bool isEnabled(covariant final T intent) => enabled; - - @override - bool consumesKey(covariant final T intent) => enabled; -}