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
+ class ComposeAutocomplete extends AutocompleteField <MentionAutocompleteQuery , MentionAutocompleteResult , User > {
12
+ ComposeAutocomplete ({
13
+ super .key,
14
+ required Narrow narrow,
15
+ required ComposeContentController controller,
16
+ required super .focusNode,
17
+ required super .fieldViewBuilder
18
+ }) : super (
19
+ controller: controller,
20
+ getAutocompleteIntent: () => controller.autocompleteIntent (),
21
+ viewModelBuilder: (context) {
22
+ final store = PerAccountStoreWidget .of (context);
23
+ return MentionAutocompleteView .init (store: store, narrow: narrow);
24
+ },
25
+ itemBuilder: (context, index, option) {
26
+ Widget avatar;
27
+ String label;
28
+ switch (option) {
29
+ case UserMentionAutocompleteResult (: var userId):
30
+ avatar = Avatar (userId: userId, size: 32 , borderRadius: 3 );
31
+ label = PerAccountStoreWidget .of (context).users[userId]! .fullName;
32
+ default :
33
+ avatar = const SizedBox ();
34
+ label = '' ;
35
+ }
36
+ return InkWell (
37
+ onTap: () {
38
+ // Probably the same intent that brought up the option that was tapped.
39
+ // If not, it still shouldn't be off by more than the time it takes
40
+ // to compute the autocomplete results, which we do asynchronously.
41
+ final intent = controller.autocompleteIntent ();
42
+ if (intent == null ) {
43
+ return ; // Shrug.
44
+ }
45
+
46
+ final store = PerAccountStoreWidget .of (context);
47
+ final String replacementString;
48
+ switch (option) {
49
+ case UserMentionAutocompleteResult (: var userId):
50
+ // TODO(i18n) language-appropriate space character; check active keyboard?
51
+ // (maybe handle centrally in `controller`)
52
+ replacementString = '${mention (store .users [userId ]!, silent : intent .query .silent , users : store .users )} ' ;
53
+ default :
54
+ replacementString = '' ;
55
+ }
56
+
57
+ controller.value = intent.textEditingValue.replaced (
58
+ TextRange (
59
+ start: intent.syntaxStart,
60
+ end: intent.textEditingValue.selection.end),
61
+ replacementString,
62
+ );
63
+ },
64
+ child: Padding (
65
+ padding: const EdgeInsets .symmetric (horizontal: 16.0 , vertical: 8.0 ),
66
+ child: Row (
67
+ children: [
68
+ avatar,
69
+ const SizedBox (width: 8 ),
70
+ Text (label)])));
71
+ });
72
+ }
73
+
74
+ class AutocompleteField <QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends StatefulWidget {
75
+ const AutocompleteField ({
12
76
super .key,
13
- required this .narrow,
14
77
required this .controller,
15
78
required this .focusNode,
16
79
required this .fieldViewBuilder,
80
+ required this .itemBuilder,
81
+ required this .viewModelBuilder,
82
+ required this .getAutocompleteIntent,
17
83
});
18
84
19
- /// The message list's narrow.
20
- final Narrow narrow;
21
-
22
- final ComposeContentController controller;
85
+ final TextEditingController controller;
23
86
final FocusNode focusNode;
24
87
final WidgetBuilder fieldViewBuilder;
88
+ final Widget Function (BuildContext , int , ResultT ) itemBuilder;
89
+ final AutocompleteView <QueryT , ResultT , CandidateT > Function (BuildContext ) viewModelBuilder;
90
+ final AutocompleteIntent <QueryT >? Function () getAutocompleteIntent;
25
91
26
92
@override
27
- State <ComposeAutocomplete > createState () => _ComposeAutocompleteState ();
93
+ State <AutocompleteField < QueryT , ResultT , CandidateT >> createState () => _AutocompleteFieldState < QueryT , ResultT , CandidateT > ();
28
94
}
29
95
30
- class _ComposeAutocompleteState extends State <ComposeAutocomplete > with PerAccountStoreAwareStateMixin <ComposeAutocomplete > {
31
- MentionAutocompleteView ? _viewModel; // TODO different autocomplete view types
96
+ class _AutocompleteFieldState < QueryT extends AutocompleteQuery , ResultT extends AutocompleteResult , CandidateT > extends State <AutocompleteField < QueryT , ResultT , CandidateT >> with PerAccountStoreAwareStateMixin <AutocompleteField < QueryT , ResultT , CandidateT > > {
97
+ AutocompleteView < QueryT , ResultT , CandidateT > ? _viewModel;
32
98
33
99
void _initViewModel () {
34
- final store = PerAccountStoreWidget .of (context);
35
- _viewModel = MentionAutocompleteView .init (store: store, narrow: widget.narrow)
100
+ _viewModel = widget.viewModelBuilder (context)
36
101
..addListener (_viewModelChanged);
37
102
}
38
103
39
- void _composeContentChanged () {
40
- final newAutocompleteIntent = widget.controller. autocompleteIntent ();
104
+ void _onChanged () {
105
+ final AutocompleteIntent < QueryT > ? newAutocompleteIntent = widget.getAutocompleteIntent ();
41
106
if (newAutocompleteIntent != null ) {
42
107
if (_viewModel == null ) {
43
108
_initViewModel ();
@@ -55,7 +120,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
55
120
@override
56
121
void initState () {
57
122
super .initState ();
58
- widget.controller.addListener (_composeContentChanged );
123
+ widget.controller.addListener (_onChanged );
59
124
}
60
125
61
126
@override
@@ -69,88 +134,43 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
69
134
}
70
135
71
136
@override
72
- void didUpdateWidget (covariant ComposeAutocomplete oldWidget) {
137
+ void didUpdateWidget (covariant AutocompleteField < QueryT , ResultT , CandidateT > oldWidget) {
73
138
super .didUpdateWidget (oldWidget);
74
139
if (widget.controller != oldWidget.controller) {
75
- oldWidget.controller.removeListener (_composeContentChanged );
76
- widget.controller.addListener (_composeContentChanged );
140
+ oldWidget.controller.removeListener (_onChanged );
141
+ widget.controller.addListener (_onChanged );
77
142
}
78
143
}
79
144
80
145
@override
81
146
void dispose () {
82
- widget.controller.removeListener (_composeContentChanged );
147
+ widget.controller.removeListener (_onChanged );
83
148
_viewModel? .dispose (); // removes our listener
84
149
super .dispose ();
85
150
}
86
151
87
- List <MentionAutocompleteResult > _resultsToDisplay = [];
152
+ List <ResultT > _resultsToDisplay = [];
88
153
89
154
void _viewModelChanged () {
90
155
setState (() {
91
156
_resultsToDisplay = _viewModel! .results.toList ();
92
157
});
93
158
}
94
159
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
- ])));
160
+ Widget _buildItem (BuildContext context, int index) {
161
+ return widget.itemBuilder (context, index, _resultsToDisplay[index]);
142
162
}
143
163
144
164
@override
145
165
Widget build (BuildContext context) {
146
- return RawAutocomplete <MentionAutocompleteResult >(
166
+ return RawAutocomplete <ResultT >(
147
167
textEditingController: widget.controller,
148
168
focusNode: widget.focusNode,
149
169
optionsBuilder: (_) => _resultsToDisplay,
150
170
optionsViewOpenDirection: OptionsViewOpenDirection .up,
151
171
// RawAutocomplete passes these when it calls optionsViewBuilder:
152
- // AutocompleteOnSelected<T > onSelected,
153
- // Iterable<T > options,
172
+ // AutocompleteOnSelected<CandidateT > onSelected,
173
+ // Iterable<CandidateT > options,
154
174
//
155
175
// We ignore them:
156
176
// - `onSelected` would cause some behavior we don't want,
@@ -159,7 +179,7 @@ class _ComposeAutocompleteState extends State<ComposeAutocomplete> with PerAccou
159
179
// the work of creating the list of options. We're not; the
160
180
// `optionsBuilder` we pass is just a function that returns
161
181
// _resultsToDisplay, which is computed with lots of help from
162
- // MentionAutocompleteView .
182
+ // AutocompleteView .
163
183
optionsViewBuilder: (context, _, __) {
164
184
return Align (
165
185
alignment: Alignment .bottomLeft,
0 commit comments