1
1
import 'package:flutter/material.dart' ;
2
2
3
+ import '../api/model/model.dart' ;
3
4
import 'content.dart' ;
4
5
import 'store.dart' ;
5
6
import '../model/autocomplete.dart' ;
6
7
import '../model/compose.dart' ;
7
8
import '../model/narrow.dart' ;
8
9
import 'compose_box.dart' ;
9
10
10
- class ComposeAutocomplete extends StatefulWidget {
11
- const ComposeAutocomplete ({
11
+ abstract class AutocompleteField < QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends StatefulWidget {
12
+ const AutocompleteField ({
12
13
super .key,
13
- required this .narrow,
14
14
required this .controller,
15
15
required this .focusNode,
16
16
required this .fieldViewBuilder,
17
17
});
18
18
19
- /// The message list's narrow.
20
- final Narrow narrow;
21
-
22
- final ComposeContentController controller;
19
+ final TextEditingController controller;
23
20
final FocusNode focusNode;
24
21
final WidgetBuilder fieldViewBuilder;
25
22
23
+ AutocompleteIntent <QueryT >? autocompleteIntent ();
24
+
25
+ Widget buildItem (BuildContext context, int index, ResultT option);
26
+
27
+ AutocompleteView <QueryT , ResultT , CandidateT > initViewModel (BuildContext context);
28
+
26
29
@override
27
- State <ComposeAutocomplete > createState () => _ComposeAutocompleteState ();
30
+ State <AutocompleteField < QueryT , ResultT , CandidateT >> createState () => _AutocompleteFieldState < QueryT , ResultT , CandidateT > ();
28
31
}
29
32
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;
32
35
33
36
void _initViewModel () {
34
- final store = PerAccountStoreWidget .of (context);
35
- _viewModel = MentionAutocompleteView .init (store: store, narrow: widget.narrow)
37
+ _viewModel = widget.initViewModel (context)
36
38
..addListener (_viewModelChanged);
37
39
}
38
40
39
- void _composeContentChanged () {
40
- final newAutocompleteIntent = widget.controller. autocompleteIntent ();
41
+ void _onChanged () {
42
+ final newAutocompleteIntent = widget.autocompleteIntent ();
41
43
if (newAutocompleteIntent != null ) {
42
44
if (_viewModel == null ) {
43
45
_initViewModel ();
@@ -55,7 +57,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
55
57
@override
56
58
void initState () {
57
59
super .initState ();
58
- widget.controller.addListener (_composeContentChanged );
60
+ widget.controller.addListener (_onChanged );
59
61
}
60
62
61
63
@override
@@ -69,88 +71,43 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
69
71
}
70
72
71
73
@override
72
- void didUpdateWidget (covariant ComposeAutocomplete oldWidget) {
74
+ void didUpdateWidget (covariant AutocompleteField < QueryT , ResultT , CandidateT > oldWidget) {
73
75
super .didUpdateWidget (oldWidget);
74
76
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 );
77
79
}
78
80
}
79
81
80
82
@override
81
83
void dispose () {
82
- widget.controller.removeListener (_composeContentChanged );
84
+ widget.controller.removeListener (_onChanged );
83
85
_viewModel? .dispose (); // removes our listener
84
86
super .dispose ();
85
87
}
86
88
87
- List <MentionAutocompleteResult > _resultsToDisplay = [];
89
+ List <ResultT > _resultsToDisplay = [];
88
90
89
91
void _viewModelChanged () {
90
92
setState (() {
91
93
_resultsToDisplay = _viewModel! .results.toList ();
92
94
});
93
95
}
94
96
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]);
142
99
}
143
100
144
101
@override
145
102
Widget build (BuildContext context) {
146
- return RawAutocomplete <MentionAutocompleteResult >(
103
+ return RawAutocomplete <ResultT >(
147
104
textEditingController: widget.controller,
148
105
focusNode: widget.focusNode,
149
106
optionsBuilder: (_) => _resultsToDisplay,
150
107
optionsViewOpenDirection: OptionsViewOpenDirection .up,
151
108
// RawAutocomplete passes these when it calls optionsViewBuilder:
152
- // AutocompleteOnSelected<T > onSelected,
153
- // Iterable<T > options,
109
+ // AutocompleteOnSelected<CandidateT > onSelected,
110
+ // Iterable<CandidateT > options,
154
111
//
155
112
// We ignore them:
156
113
// - `onSelected` would cause some behavior we don't want,
@@ -159,7 +116,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
159
116
// the work of creating the list of options. We're not; the
160
117
// `optionsBuilder` we pass is just a function that returns
161
118
// _resultsToDisplay, which is computed with lots of help from
162
- // MentionAutocompleteView .
119
+ // AutocompleteView .
163
120
optionsViewBuilder: (context, _, __) {
164
121
return Align (
165
122
alignment: Alignment .bottomLeft,
@@ -187,3 +144,76 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
187
144
);
188
145
}
189
146
}
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