import 'dart:async'; import 'package:flutter/material.dart'; import 'package:em2rp/services/travel_service.dart'; /// Champ texte avec autocomplétion d'adresses via Google Places. /// Remplace le TextFormField standard pour le champ adresse. class AddressAutocompleteField extends StatefulWidget { final TextEditingController controller; final String label; final String? Function(String?)? validator; final void Function(String)? onSelected; final InputDecoration? decoration; const AddressAutocompleteField({ super.key, required this.controller, this.label = 'Adresse', this.validator, this.onSelected, this.decoration, }); @override State createState() => _AddressAutocompleteFieldState(); } class _AddressAutocompleteFieldState extends State { final _service = TravelService(); final _focusNode = FocusNode(); final _overlayKey = GlobalKey(); List _suggestions = []; Timer? _debounce; OverlayEntry? _overlayEntry; final _layerLink = LayerLink(); @override void initState() { super.initState(); widget.controller.addListener(_onTextChanged); _focusNode.addListener(() { if (!_focusNode.hasFocus) { _removeOverlay(); } }); } void _onTextChanged() { _debounce?.cancel(); final text = widget.controller.text; if (text.length < 3) { _removeOverlay(); return; } _debounce = Timer(const Duration(milliseconds: 400), () async { final results = await _service.autocompleteAddress(text); if (mounted) { setState(() => _suggestions = results); if (results.isNotEmpty && _focusNode.hasFocus) { _showSuggestions(); } else { _removeOverlay(); } } }); } void _showSuggestions() { _removeOverlay(); final overlay = Overlay.of(context); _overlayEntry = OverlayEntry( builder: (ctx) => Positioned( width: _getFieldWidth(), child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, offset: const Offset(0, 56), child: Material( elevation: 6, borderRadius: BorderRadius.circular(8), child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 220), child: ListView.separated( shrinkWrap: true, padding: EdgeInsets.zero, itemCount: _suggestions.length, separatorBuilder: (_, __) => const Divider(height: 1, indent: 16), itemBuilder: (ctx, i) { return ListTile( dense: true, leading: const Icon(Icons.location_on_outlined, size: 18), title: Text( _suggestions[i], style: const TextStyle(fontSize: 13), overflow: TextOverflow.ellipsis, ), onTap: () { widget.controller.text = _suggestions[i]; widget.controller.selection = TextSelection.fromPosition( TextPosition(offset: _suggestions[i].length), ); widget.onSelected?.call(_suggestions[i]); _removeOverlay(); _focusNode.unfocus(); }, ); }, ), ), ), ), ), ); overlay.insert(_overlayEntry!); setState(() => _showOverlay = true); } double _getFieldWidth() { final rb = context.findRenderObject() as RenderBox?; return rb?.size.width ?? 300; } void _removeOverlay() { _overlayEntry?.remove(); _overlayEntry = null; if (mounted) setState(() => _showOverlay = false); } @override void dispose() { _debounce?.cancel(); widget.controller.removeListener(_onTextChanged); _focusNode.dispose(); _removeOverlay(); super.dispose(); } @override Widget build(BuildContext context) { return CompositedTransformTarget( link: _layerLink, child: TextFormField( key: _overlayKey, controller: widget.controller, focusNode: _focusNode, decoration: widget.decoration ?? InputDecoration( labelText: widget.label, border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.location_on_outlined), suffixIcon: widget.controller.text.isNotEmpty ? IconButton( icon: const Icon(Icons.clear, size: 18), onPressed: () { widget.controller.clear(); _removeOverlay(); }, ) : null, ), validator: widget.validator, onChanged: (_) {}, ), ); } }