Search bar improved: alignment option added, search settings changed from icons to checkboxes with titles, search happens on the fly, other minor improvements

pull/1904/head
Alspb 11 months ago
parent 6a0c101443
commit c23cb4eab9
  1. 4
      lib/src/models/config/toolbar/buttons/search_configurations.dart
  2. 1
      lib/src/widgets/raw_editor/raw_editor_actions.dart
  3. 4
      lib/src/widgets/toolbar/buttons/search/search_button.dart
  4. 300
      lib/src/widgets/toolbar/buttons/search/search_dialog.dart

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart' show Color; import 'package:flutter/widgets.dart' show Color;
import '../../../../../flutter_quill.dart'; import '../../../../../flutter_quill.dart';
@ -24,6 +25,7 @@ class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions<
super.iconButtonFactor, super.iconButtonFactor,
this.dialogBarrierColor, this.dialogBarrierColor,
this.customOnPressedCallback, this.customOnPressedCallback,
this.searchBarAlignment,
}); });
final QuillDialogTheme? dialogTheme; final QuillDialogTheme? dialogTheme;
@ -34,6 +36,8 @@ class QuillToolbarSearchButtonOptions extends QuillToolbarBaseButtonOptions<
/// By default we will show simple search dialog ui /// By default we will show simple search dialog ui
/// you can pass value to this callback to change this /// you can pass value to this callback to change this
final QuillToolbarSearchButtonOnPressedCallback? customOnPressedCallback; final QuillToolbarSearchButtonOnPressedCallback? customOnPressedCallback;
final AlignmentGeometry? searchBarAlignment;
} }
typedef QuillToolbarSearchButtonOnPressedCallback = Future<void> Function( typedef QuillToolbarSearchButtonOnPressedCallback = Future<void> Function(

@ -462,7 +462,6 @@ class QuillEditorOpenSearchAction extends ContextAction<OpenSearchIntent> {
context: context, context: context,
builder: (_) => QuillToolbarSearchDialog( builder: (_) => QuillToolbarSearchDialog(
controller: state.controller, controller: state.controller,
text: '',
), ),
); );
} }

@ -124,13 +124,13 @@ class QuillToolbarSearchButton extends StatelessWidget {
return; return;
} }
await showDialog<String>( await showDialog<String>(
barrierColor: _dialogBarrierColor(context), barrierColor: Colors.transparent,
context: context, context: context,
builder: (_) => FlutterQuillLocalizationsWidget( builder: (_) => FlutterQuillLocalizationsWidget(
child: QuillToolbarSearchDialog( child: QuillToolbarSearchDialog(
controller: controller, controller: controller,
dialogTheme: _dialogTheme(context), dialogTheme: _dialogTheme(context),
text: '', searchBarAlignment: options.searchBarAlignment,
), ),
), ),
); );

@ -1,5 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../../../extensions.dart';
import '../../../../l10n/extensions/localizations.dart'; import '../../../../l10n/extensions/localizations.dart';
import '../../../../l10n/widgets/localizations.dart'; import '../../../../l10n/widgets/localizations.dart';
import '../../../../models/documents/document.dart'; import '../../../../models/documents/document.dart';
@ -42,15 +45,15 @@ class QuillToolbarSearchDialog extends StatefulWidget {
const QuillToolbarSearchDialog({ const QuillToolbarSearchDialog({
required this.controller, required this.controller,
this.dialogTheme, this.dialogTheme,
this.text,
this.childBuilder, this.childBuilder,
this.searchBarAlignment,
super.key, super.key,
}); });
final QuillController controller; final QuillController controller;
final QuillDialogTheme? dialogTheme; final QuillDialogTheme? dialogTheme;
final String? text;
final QuillToolbarSearchDialogChildBuilder? childBuilder; final QuillToolbarSearchDialogChildBuilder? childBuilder;
final AlignmentGeometry? searchBarAlignment;
@override @override
QuillToolbarSearchDialogState createState() => QuillToolbarSearchDialogState createState() =>
@ -58,39 +61,24 @@ class QuillToolbarSearchDialog extends StatefulWidget {
} }
class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> { class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
late String _text; final TextEditingController _textController = TextEditingController();
late TextEditingController _controller; String _text = '';
late List<int>? _offsets; List<int> _offsets = [];
late int _index; int _index = 0;
bool _caseSensitive = false; bool _caseSensitive = false;
bool _wholeWord = false; bool _wholeWord = false;
bool _searchSettingsUnfolded = false;
@override Timer? _searchTimer;
void initState() {
super.initState();
_text = widget.text ?? '';
_offsets = null;
_index = 0;
_controller = TextEditingController(text: _text);
}
@override @override
void dispose() { void dispose() {
_controller.dispose(); _textController.dispose();
_searchTimer?.cancel();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var matchShown = '';
if (_offsets != null) {
if (_offsets!.isEmpty) {
matchShown = '0/0';
} else {
matchShown = '${_index + 1}/${_offsets!.length}';
}
}
final childBuilder = widget.childBuilder; final childBuilder = widget.childBuilder;
if (childBuilder != null) { if (childBuilder != null) {
return childBuilder( return childBuilder(
@ -99,7 +87,7 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
onEditingComplete: _findText, onEditingComplete: _findText,
onTextChanged: _textChanged, onTextChanged: _textChanged,
caseSensitive: _caseSensitive, caseSensitive: _caseSensitive,
textEditingController: _controller, textEditingController: _textController,
index: _index, index: _index,
offsets: _offsets, offsets: _offsets,
text: _text, text: _text,
@ -110,90 +98,139 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
); );
} }
final searchBarAlignment =
widget.searchBarAlignment ?? Alignment.bottomCenter;
final searchBarAtBottom = (searchBarAlignment == Alignment.bottomCenter) ||
(searchBarAlignment == Alignment.bottomLeft) ||
(searchBarAlignment == Alignment.bottomRight);
final addBottomPadding = searchBarAtBottom && isMobile(supportWeb: true);
var matchShown = '';
if (_text.isNotEmpty) {
if (_offsets.isEmpty) {
matchShown = '0/0';
} else {
matchShown = '${_index + 1}/${_offsets.length}';
}
}
final searchBar = Container(
height: addBottomPadding ? 50 : 45,
padding: addBottomPadding ? const EdgeInsets.only(bottom: 12) : null,
child: Row(
children: [
IconButton(
icon: const Icon(Icons.close),
tooltip: 'Close',
visualDensity: VisualDensity.compact,
onPressed: () {
Navigator.of(context).pop();
},
),
IconButton(
icon: const Icon(Icons.more_vert),
isSelected: _caseSensitive || _wholeWord,
tooltip: 'Search settings',
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() {
_searchSettingsUnfolded = !_searchSettingsUnfolded;
});
},
),
Expanded(
child: TextField(
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
isDense: true,
suffixText: matchShown,
suffixStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _textChanged,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.text,
controller: _textController,
),
),
IconButton(
icon: const Icon(Icons.keyboard_arrow_up),
tooltip: context.loc.moveToPreviousOccurrence,
onPressed: (_offsets.isNotEmpty) ? _moveToPrevious : null,
),
IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
tooltip: context.loc.moveToNextOccurrence,
onPressed: (_offsets.isNotEmpty) ? _moveToNext : null,
),
],
),
);
final searchSettings = SizedBox(
height: 45,
child: Row(
children: [
Expanded(
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
visualDensity: VisualDensity.compact,
contentPadding: EdgeInsets.zero,
title: const Text(
'Case sensitive',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
value: _caseSensitive,
onChanged: (value) {
setState(() {
_caseSensitive = value!;
_findText();
});
},
),
),
Expanded(
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
visualDensity: VisualDensity.compact,
contentPadding: EdgeInsets.zero,
title: const Text(
'Whole word',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
value: _wholeWord,
onChanged: (value) {
setState(() {
_wholeWord = value!;
_findText();
});
},
),
),
],
),
);
return Dialog( return Dialog(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
backgroundColor: widget.dialogTheme?.dialogBackgroundColor, backgroundColor: widget.dialogTheme?.dialogBackgroundColor,
alignment: Alignment.bottomCenter, alignment: searchBarAlignment,
insetPadding: EdgeInsets.zero, insetPadding: EdgeInsets.zero,
child: FlutterQuillLocalizationsWidget( child: FlutterQuillLocalizationsWidget(
child: Builder( child: Builder(
builder: (context) { builder: (context) {
return SizedBox( return Column(
height: 45, mainAxisSize: MainAxisSize.min,
child: Row( children: [
children: [ if (_searchSettingsUnfolded && searchBarAtBottom)
Tooltip( searchSettings,
message: context.loc.caseSensitivityAndWholeWordSearch, searchBar,
child: ToggleButtons( if (_searchSettingsUnfolded && !searchBarAtBottom)
onPressed: (index) { searchSettings,
if (index == 0) { ],
_changeCaseSensitivity();
} else if (index == 1) {
_changeWholeWord();
}
},
borderRadius: const BorderRadius.all(Radius.circular(2)),
isSelected: [_caseSensitive, _wholeWord],
children: const [
Text(
'\u0391\u03b1',
style: TextStyle(
fontFamily: 'MaterialIcons',
fontSize: 24,
),
),
Text(
'\u201c\u2026\u201d',
style: TextStyle(
fontFamily: 'MaterialIcons',
fontSize: 24,
),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 12, left: 5),
child: TextField(
style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration(
isDense: true,
suffixText: (_offsets != null) ? matchShown : '',
suffixStyle: widget.dialogTheme?.labelTextStyle,
),
autofocus: true,
onChanged: _textChanged,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.text,
onEditingComplete: _findText,
controller: _controller,
),
),
),
if (_offsets == null)
IconButton(
icon: const Icon(Icons.search),
tooltip: context.loc.findText,
onPressed: _findText,
),
if (_offsets != null)
IconButton(
icon: const Icon(Icons.keyboard_arrow_up),
tooltip: context.loc.moveToPreviousOccurrence,
onPressed:
(_offsets!.isNotEmpty) ? _moveToPrevious : null,
),
if (_offsets != null)
IconButton(
icon: const Icon(Icons.keyboard_arrow_down),
tooltip: context.loc.moveToNextOccurrence,
onPressed: (_offsets!.isNotEmpty) ? _moveToNext : null,
),
],
),
); );
}, },
), ),
@ -201,9 +238,30 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
); );
} }
void _textChanged(String text) {
_text = text;
if (_searchTimer?.isActive ?? false) {
_searchTimer?.cancel();
}
_searchTimer = Timer(
const Duration(milliseconds: 300),
_findText,
);
}
void _findText() { void _findText() {
_text = _controller.text;
if (_text.isEmpty) { if (_text.isEmpty) {
setState(() {
_offsets = [];
_index = 0;
widget.controller.updateSelection(
TextSelection(
baseOffset: widget.controller.selection.baseOffset,
extentOffset: widget.controller.selection.baseOffset,
),
ChangeSource.local,
);
});
return; return;
} }
setState(() { setState(() {
@ -214,7 +272,7 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
); );
_index = 0; _index = 0;
}); });
if (_offsets!.isNotEmpty) { if (_offsets.isNotEmpty) {
_moveToPosition(); _moveToPosition();
} }
} }
@ -222,33 +280,33 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
void _moveToPosition() { void _moveToPosition() {
widget.controller.updateSelection( widget.controller.updateSelection(
TextSelection( TextSelection(
baseOffset: _offsets![_index], baseOffset: _offsets[_index],
extentOffset: _offsets![_index] + _text.length, extentOffset: _offsets[_index] + _text.length,
), ),
ChangeSource.local, ChangeSource.local,
); );
} }
void _moveToPrevious() { void _moveToPrevious() {
if (_offsets!.isEmpty) { if (_offsets.isEmpty) {
return; return;
} }
setState(() { setState(() {
if (_index > 0) { if (_index > 0) {
_index -= 1; _index -= 1;
} else { } else {
_index = _offsets!.length - 1; _index = _offsets.length - 1;
} }
}); });
_moveToPosition(); _moveToPosition();
} }
void _moveToNext() { void _moveToNext() {
if (_offsets!.isEmpty) { if (_offsets.isEmpty) {
return; return;
} }
setState(() { setState(() {
if (_index < _offsets!.length - 1) { if (_index < _offsets.length - 1) {
_index += 1; _index += 1;
} else { } else {
_index = 0; _index = 0;
@ -256,28 +314,4 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
}); });
_moveToPosition(); _moveToPosition();
} }
void _textChanged(String value) {
setState(() {
_text = value;
_offsets = null;
_index = 0;
});
}
void _changeCaseSensitivity() {
setState(() {
_caseSensitive = !_caseSensitive;
_offsets = null;
_index = 0;
});
}
void _changeWholeWord() {
setState(() {
_wholeWord = !_wholeWord;
_offsets = null;
_index = 0;
});
}
} }

Loading…
Cancel
Save