diff --git a/lib/dev_config.dart b/lib/dev_config.dart index ad0ea52..729efff 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -144,6 +144,7 @@ const int kDefaultMaxNodes = 500; // Default maximum number of nodes to render o // NSI (Name Suggestion Index) configuration const int kNSIMinimumHitCount = 500; // Minimum hit count for NSI suggestions to be considered useful +const int kNSIMaxSuggestions = 10; // Maximum number of tag value suggestions to fetch and display // Map interaction configuration const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0) diff --git a/lib/services/nsi_service.dart b/lib/services/nsi_service.dart index f799d26..79f63e1 100644 --- a/lib/services/nsi_service.dart +++ b/lib/services/nsi_service.dart @@ -84,8 +84,7 @@ class NSIService { } } - // Limit to top 10 suggestions for UI performance - if (suggestions.length >= 10) break; + if (suggestions.length >= kNSIMaxSuggestions) break; } return suggestions; diff --git a/lib/widgets/nsi_tag_value_field.dart b/lib/widgets/nsi_tag_value_field.dart index 1f33c71..51685f1 100644 --- a/lib/widgets/nsi_tag_value_field.dart +++ b/lib/widgets/nsi_tag_value_field.dart @@ -25,33 +25,26 @@ class NSITagValueField extends StatefulWidget { class _NSITagValueFieldState extends State { late TextEditingController _controller; - List _suggestions = []; - bool _showingSuggestions = false; - final LayerLink _layerLink = LayerLink(); - late OverlayEntry _overlayEntry; final FocusNode _focusNode = FocusNode(); + List _suggestions = []; @override void initState() { super.initState(); _controller = TextEditingController(text: widget.initialValue); _loadSuggestions(); - - _focusNode.addListener(_onFocusChanged); - _controller.addListener(_onTextChanged); } @override void didUpdateWidget(NSITagValueField oldWidget) { super.didUpdateWidget(oldWidget); - + // If the tag key changed, reload suggestions if (oldWidget.tagKey != widget.tagKey) { - _hideSuggestions(); // Hide old suggestions immediately _suggestions.clear(); - _loadSuggestions(); // Load new suggestions for new key + _loadSuggestions(); } - + // If the initial value changed, update the controller if (oldWidget.initialValue != widget.initialValue) { _controller.text = widget.initialValue; @@ -62,38 +55,17 @@ class _NSITagValueFieldState extends State { void dispose() { _controller.dispose(); _focusNode.dispose(); - _hideSuggestions(); super.dispose(); } - /// Get filtered suggestions based on current text input (case-sensitive) - List _getFilteredSuggestions() { - final currentText = _controller.text; - if (currentText.isEmpty) { - return _suggestions; - } - - return _suggestions - .where((suggestion) => suggestion.contains(currentText)) - .toList(); - } - - /// Handle text changes to update suggestion filtering - void _onTextChanged() { - if (_showingSuggestions) { - // Update the overlay with filtered suggestions - _updateSuggestionsOverlay(); - } - } - void _loadSuggestions() async { if (widget.tagKey.trim().isEmpty) return; - + try { final suggestions = await NSIService().getAllSuggestions(widget.tagKey); if (mounted) { setState(() { - _suggestions = suggestions.take(10).toList(); // Limit to 10 suggestions + _suggestions = suggestions; }); } } catch (e) { @@ -106,52 +78,71 @@ class _NSITagValueFieldState extends State { } } - void _onFocusChanged() { - final filteredSuggestions = _getFilteredSuggestions(); - if (_focusNode.hasFocus && filteredSuggestions.isNotEmpty && !widget.readOnly) { - _showSuggestions(); - } else { - _hideSuggestions(); - } + InputDecoration _buildDecoration({required bool showDropdownIcon}) { + return InputDecoration( + hintText: widget.hintText, + border: const OutlineInputBorder(), + isDense: true, + suffixIcon: showDropdownIcon + ? Icon( + Icons.arrow_drop_down, + color: _focusNode.hasFocus + ? Theme.of(context).primaryColor + : Colors.grey, + ) + : null, + ); } - void _showSuggestions() { - final filteredSuggestions = _getFilteredSuggestions(); - if (_showingSuggestions || filteredSuggestions.isEmpty) return; - - _overlayEntry = _buildSuggestionsOverlay(filteredSuggestions); - Overlay.of(context).insert(_overlayEntry); - setState(() { - _showingSuggestions = true; - }); - } - - /// Update the suggestions overlay with current filtered suggestions - void _updateSuggestionsOverlay() { - final filteredSuggestions = _getFilteredSuggestions(); - - if (filteredSuggestions.isEmpty) { - _hideSuggestions(); - return; + @override + Widget build(BuildContext context) { + if (widget.readOnly) { + return TextField( + controller: _controller, + focusNode: _focusNode, + readOnly: true, + decoration: _buildDecoration(showDropdownIcon: false), + ); } - - if (_showingSuggestions) { - // Remove current overlay and create new one with filtered suggestions - _overlayEntry.remove(); - _overlayEntry = _buildSuggestionsOverlay(filteredSuggestions); - Overlay.of(context).insert(_overlayEntry); - } - } - /// Build the suggestions overlay with the given suggestions list - OverlayEntry _buildSuggestionsOverlay(List suggestions) { - return OverlayEntry( - builder: (context) => Positioned( - width: 250, // Slightly wider to fit more content in refine tags - child: CompositedTransformFollower( - link: _layerLink, - showWhenUnlinked: false, - offset: const Offset(0.0, 35.0), // Below the text field + return RawAutocomplete( + textEditingController: _controller, + focusNode: _focusNode, + optionsBuilder: (TextEditingValue textEditingValue) { + if (_suggestions.isEmpty) return const Iterable.empty(); + if (textEditingValue.text.isEmpty) return _suggestions; + return _suggestions + .where((s) => s.contains(textEditingValue.text)); + }, + onSelected: (String selection) { + widget.onChanged(selection); + }, + fieldViewBuilder: ( + BuildContext context, + TextEditingController controller, + FocusNode focusNode, + VoidCallback onFieldSubmitted, + ) { + return TextField( + controller: controller, + focusNode: focusNode, + decoration: _buildDecoration( + showDropdownIcon: _suggestions.isNotEmpty, + ), + onChanged: (value) { + widget.onChanged(value); + }, + onSubmitted: (_) => onFieldSubmitted(), + ); + }, + optionsViewBuilder: ( + BuildContext context, + AutocompleteOnSelected onSelected, + Iterable options, + ) { + final optionList = options.toList(growable: false); + return Align( + alignment: Alignment.topLeft, child: Material( elevation: 4.0, borderRadius: BorderRadius.circular(8.0), @@ -160,68 +151,20 @@ class _NSITagValueFieldState extends State { child: ListView.builder( padding: EdgeInsets.zero, shrinkWrap: true, - itemCount: suggestions.length, + itemCount: optionList.length, itemBuilder: (context, index) { - final suggestion = suggestions[index]; + final option = optionList[index]; return ListTile( dense: true, - title: Text(suggestion, style: const TextStyle(fontSize: 14)), - onTap: () => _selectSuggestion(suggestion), + title: Text(option, style: const TextStyle(fontSize: 14)), + onTap: () => onSelected(option), ); }, ), ), ), - ), - ), + ); + }, ); } - - void _hideSuggestions() { - if (!_showingSuggestions) return; - - _overlayEntry.remove(); - setState(() { - _showingSuggestions = false; - }); - } - - void _selectSuggestion(String suggestion) { - _controller.text = suggestion; - widget.onChanged(suggestion); - _hideSuggestions(); - } - - @override - Widget build(BuildContext context) { - final filteredSuggestions = _getFilteredSuggestions(); - - return CompositedTransformTarget( - link: _layerLink, - child: TextField( - controller: _controller, - focusNode: _focusNode, - readOnly: widget.readOnly, - decoration: InputDecoration( - hintText: widget.hintText, - border: const OutlineInputBorder(), - isDense: true, - suffixIcon: _suggestions.isNotEmpty && !widget.readOnly - ? Icon( - Icons.arrow_drop_down, - color: _showingSuggestions ? Theme.of(context).primaryColor : Colors.grey, - ) - : null, - ), - onChanged: widget.readOnly ? null : (value) { - widget.onChanged(value); - }, - onTap: () { - if (!widget.readOnly && filteredSuggestions.isNotEmpty) { - _showSuggestions(); - } - }, - ), - ); - } -} \ No newline at end of file +}