Skip to content

Commit 540df4e

Browse files
committed
autocomplete [nfc]: Factor out generic AutocompleteField
Most of the logic in `ComposeAutocomplete` is not specific to the content input it self rather it is general logic that applies to any autocomplete field.
1 parent 4d18990 commit 540df4e

File tree

1 file changed

+101
-71
lines changed

1 file changed

+101
-71
lines changed

lib/widgets/autocomplete.dart

Lines changed: 101 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,45 @@
11
import 'package:flutter/material.dart';
22

3+
import '../api/model/model.dart';
34
import 'content.dart';
45
import 'store.dart';
56
import '../model/autocomplete.dart';
67
import '../model/compose.dart';
78
import '../model/narrow.dart';
89
import 'compose_box.dart';
910

10-
class ComposeAutocomplete extends StatefulWidget {
11-
const ComposeAutocomplete({
11+
abstract class AutocompleteField<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult, CandidateT> extends StatefulWidget {
12+
const AutocompleteField({
1213
super.key,
13-
required this.narrow,
1414
required this.controller,
1515
required this.focusNode,
1616
required this.fieldViewBuilder,
1717
});
1818

19-
/// The message list's narrow.
20-
final Narrow narrow;
21-
22-
final ComposeContentController controller;
19+
final TextEditingController controller;
2320
final FocusNode focusNode;
2421
final WidgetBuilder fieldViewBuilder;
2522

23+
AutocompleteIntent<QueryT>? autocompleteIntent();
24+
25+
Widget buildItem(BuildContext context, int index, ResultT option);
26+
27+
AutocompleteView<QueryT, ResultT, CandidateT> initViewModel(BuildContext context);
28+
2629
@override
27-
State<ComposeAutocomplete> createState() => _ComposeAutocompleteState();
30+
State<AutocompleteField<QueryT, ResultT, CandidateT>> createState() => _AutocompleteFieldState<QueryT, ResultT, CandidateT>();
2831
}
2932

30-
class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccountStoreAwareStateMixin<ComposeAutocomplete> {
31-
MentionAutocompleteView? _viewModel; // TODO different autocomplete view types
33+
class _AutocompleteFieldState<QueryT extends AutocompleteQuery, ResultT extends AutocompleteResult, CandidateT> extends State<AutocompleteField<QueryT, ResultT, CandidateT>> with PerAccountStoreAwareStateMixin<AutocompleteField<QueryT, ResultT, CandidateT>> {
34+
AutocompleteView<QueryT, ResultT, CandidateT>? _viewModel;
3235

3336
void _initViewModel() {
34-
final store = PerAccountStoreWidget.of(context);
35-
_viewModel = MentionAutocompleteView.init(store: store, narrow: widget.narrow)
37+
_viewModel = widget.initViewModel(context)
3638
..addListener(_viewModelChanged);
3739
}
3840

39-
void _composeContentChanged() {
40-
final newAutocompleteIntent = widget.controller.autocompleteIntent();
41+
void _onChanged() {
42+
final newAutocompleteIntent = widget.autocompleteIntent();
4143
if (newAutocompleteIntent != null) {
4244
if (_viewModel == null) {
4345
_initViewModel();
@@ -55,7 +57,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
5557
@override
5658
void initState() {
5759
super.initState();
58-
widget.controller.addListener(_composeContentChanged);
60+
widget.controller.addListener(_onChanged);
5961
}
6062

6163
@override
@@ -69,88 +71,43 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
6971
}
7072

7173
@override
72-
void didUpdateWidget(covariant ComposeAutocomplete oldWidget) {
74+
void didUpdateWidget(covariant AutocompleteField<QueryT, ResultT, CandidateT> oldWidget) {
7375
super.didUpdateWidget(oldWidget);
7476
if (widget.controller != oldWidget.controller) {
75-
oldWidget.controller.removeListener(_composeContentChanged);
76-
widget.controller.addListener(_composeContentChanged);
77+
oldWidget.controller.removeListener(_onChanged);
78+
widget.controller.addListener(_onChanged);
7779
}
7880
}
7981

8082
@override
8183
void dispose() {
82-
widget.controller.removeListener(_composeContentChanged);
84+
widget.controller.removeListener(_onChanged);
8385
_viewModel?.dispose(); // removes our listener
8486
super.dispose();
8587
}
8688

87-
List<MentionAutocompleteResult> _resultsToDisplay = [];
89+
List<ResultT> _resultsToDisplay = [];
8890

8991
void _viewModelChanged() {
9092
setState(() {
9193
_resultsToDisplay = _viewModel!.results.toList();
9294
});
9395
}
9496

95-
void _onTapOption(MentionAutocompleteResult option) {
96-
// Probably the same intent that brought up the option that was tapped.
97-
// If not, it still shouldn't be off by more than the time it takes
98-
// to compute the autocomplete results, which we do asynchronously.
99-
final intent = widget.controller.autocompleteIntent();
100-
if (intent == null) {
101-
return; // Shrug.
102-
}
103-
104-
final store = PerAccountStoreWidget.of(context);
105-
final String replacementString;
106-
switch (option) {
107-
case UserMentionAutocompleteResult(:var userId):
108-
// TODO(i18n) language-appropriate space character; check active keyboard?
109-
// (maybe handle centrally in `widget.controller`)
110-
replacementString = '${mention(store.users[userId]!, silent: intent.query.silent, users: store.users)} ';
111-
}
112-
113-
widget.controller.value = intent.textEditingValue.replaced(
114-
TextRange(
115-
start: intent.syntaxStart,
116-
end: intent.textEditingValue.selection.end),
117-
replacementString,
118-
);
119-
}
120-
121-
Widget _buildItem(BuildContext _, int index) {
122-
final option = _resultsToDisplay[index];
123-
Widget avatar;
124-
String label;
125-
switch (option) {
126-
case UserMentionAutocompleteResult(:var userId):
127-
avatar = Avatar(userId: userId, size: 32, borderRadius: 3);
128-
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
129-
}
130-
return InkWell(
131-
onTap: () {
132-
_onTapOption(option);
133-
},
134-
child: Padding(
135-
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
136-
child: Row(
137-
children: [
138-
avatar,
139-
const SizedBox(width: 8),
140-
Text(label),
141-
])));
97+
Widget _buildItem(BuildContext context, int index) {
98+
return widget.buildItem(context, index, _resultsToDisplay[index]);
14299
}
143100

144101
@override
145102
Widget build(BuildContext context) {
146-
return RawAutocomplete<MentionAutocompleteResult>(
103+
return RawAutocomplete<ResultT>(
147104
textEditingController: widget.controller,
148105
focusNode: widget.focusNode,
149106
optionsBuilder: (_) => _resultsToDisplay,
150107
optionsViewOpenDirection: OptionsViewOpenDirection.up,
151108
// RawAutocomplete passes these when it calls optionsViewBuilder:
152-
// AutocompleteOnSelected<T> onSelected,
153-
// Iterable<T> options,
109+
// AutocompleteOnSelected<CandidateT> onSelected,
110+
// Iterable<CandidateT> options,
154111
//
155112
// We ignore them:
156113
// - `onSelected` would cause some behavior we don't want,
@@ -159,7 +116,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
159116
// the work of creating the list of options. We're not; the
160117
// `optionsBuilder` we pass is just a function that returns
161118
// _resultsToDisplay, which is computed with lots of help from
162-
// MentionAutocompleteView.
119+
// AutocompleteView.
163120
optionsViewBuilder: (context, _, __) {
164121
return Align(
165122
alignment: Alignment.bottomLeft,
@@ -187,3 +144,76 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
187144
);
188145
}
189146
}
147+
148+
class ComposeAutocomplete extends AutocompleteField<MentionAutocompleteQuery, MentionAutocompleteResult, User> {
149+
const ComposeAutocomplete({
150+
super.key,
151+
required this.narrow,
152+
required super.focusNode,
153+
required super.fieldViewBuilder,
154+
required ComposeContentController controller,
155+
}) : super(controller: controller);
156+
157+
final Narrow narrow;
158+
159+
@override
160+
ComposeContentController get controller => super.controller as ComposeContentController;
161+
162+
@override
163+
AutocompleteIntent<MentionAutocompleteQuery>? autocompleteIntent() => controller.autocompleteIntent();
164+
165+
@override
166+
AutocompleteView<MentionAutocompleteQuery, MentionAutocompleteResult, User> initViewModel(BuildContext context) {
167+
final store = PerAccountStoreWidget.of(context);
168+
return MentionAutocompleteView.init(store: store, narrow: narrow);
169+
}
170+
171+
@override
172+
Widget buildItem(BuildContext context, int index, MentionAutocompleteResult option) {
173+
Widget avatar;
174+
String label;
175+
switch (option) {
176+
case UserMentionAutocompleteResult(:var userId):
177+
avatar = Avatar(userId: userId, size: 32, borderRadius: 3);
178+
label = PerAccountStoreWidget.of(context).users[userId]!.fullName;
179+
default:
180+
avatar = const SizedBox();
181+
label = '';
182+
}
183+
return InkWell(
184+
onTap: () {
185+
// Probably the same intent that brought up the option that was tapped.
186+
// If not, it still shouldn't be off by more than the time it takes
187+
// to compute the autocomplete results, which we do asynchronously.
188+
final intent = autocompleteIntent();
189+
if (intent == null) {
190+
return; // Shrug.
191+
}
192+
193+
final store = PerAccountStoreWidget.of(context);
194+
final String replacementString;
195+
switch (option) {
196+
case UserMentionAutocompleteResult(:var userId):
197+
// TODO(i18n) language-appropriate space character; check active keyboard?
198+
// (maybe handle centrally in `controller`)
199+
replacementString = '${mention(store.users[userId]!, silent: intent.query.silent, users: store.users)} ';
200+
default:
201+
replacementString = '';
202+
}
203+
204+
controller.value = intent.textEditingValue.replaced(
205+
TextRange(
206+
start: intent.syntaxStart,
207+
end: intent.textEditingValue.selection.end),
208+
replacementString,
209+
);
210+
},
211+
child: Padding(
212+
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
213+
child: Row(
214+
children: [
215+
avatar,
216+
const SizedBox(width: 8),
217+
Text(label)])));
218+
}
219+
}

0 commit comments

Comments
 (0)