Skip to content

Commit c20e168

Browse files
authored
CupertinoTextfield control (#2417)
* adaptive prop * added CupertinoTextField * reformat cupertino_textfield.py * TextField.show_cursor * reformat * decoration props * parseVisibilityMode * export VisibilityMode
1 parent ed672c5 commit c20e168

File tree

8 files changed

+734
-7
lines changed

8 files changed

+734
-7
lines changed

package/lib/src/controls/create_control.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import 'cupertino_navigation_bar.dart';
3636
import 'cupertino_radio.dart';
3737
import 'cupertino_slider.dart';
3838
import 'cupertino_switch.dart';
39+
import 'cupertino_textfield.dart';
3940
import 'datatable.dart';
4041
import 'date_picker.dart';
4142
import 'dismissible.dart';
@@ -519,6 +520,13 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent,
519520
control: controlView.control,
520521
children: controlView.children,
521522
parentDisabled: parentDisabled);
523+
case "cupertinotextfield":
524+
return CupertinoTextFieldControl(
525+
key: key,
526+
parent: parent,
527+
control: controlView.control,
528+
children: controlView.children,
529+
parentDisabled: parentDisabled);
522530
case "searchbar":
523531
return SearchAnchorControl(
524532
key: key,
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:flet/src/controls/textfield.dart';
3+
import 'package:flutter/cupertino.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
6+
import 'package:flutter_redux/flutter_redux.dart';
7+
8+
import '../actions.dart';
9+
import '../flet_app_services.dart';
10+
import '../models/app_state.dart';
11+
import '../models/control.dart';
12+
import '../protocol/update_control_props_payload.dart';
13+
import '../utils/borders.dart';
14+
import '../utils/colors.dart';
15+
import '../utils/gradient.dart';
16+
import '../utils/shadows.dart';
17+
import '../utils/text.dart';
18+
import '../utils/textfield.dart';
19+
import 'create_control.dart';
20+
import 'form_field.dart';
21+
22+
class CupertinoTextFieldControl extends StatefulWidget {
23+
final Control? parent;
24+
final Control control;
25+
final List<Control> children;
26+
final bool parentDisabled;
27+
28+
const CupertinoTextFieldControl(
29+
{super.key,
30+
this.parent,
31+
required this.control,
32+
required this.children,
33+
required this.parentDisabled});
34+
35+
@override
36+
State<CupertinoTextFieldControl> createState() =>
37+
_CupertinoTextFieldControlState();
38+
}
39+
40+
class _CupertinoTextFieldControlState extends State<CupertinoTextFieldControl> {
41+
String _value = "";
42+
bool _revealPassword = false;
43+
bool _focused = false;
44+
late TextEditingController _controller;
45+
late final FocusNode _focusNode;
46+
late final FocusNode _shiftEnterfocusNode;
47+
String? _lastFocusValue;
48+
49+
@override
50+
void initState() {
51+
super.initState();
52+
_controller = TextEditingController();
53+
_shiftEnterfocusNode = FocusNode(
54+
onKey: (FocusNode node, RawKeyEvent evt) {
55+
if (!evt.isShiftPressed && evt.logicalKey.keyLabel == 'Enter') {
56+
if (evt is RawKeyDownEvent) {
57+
FletAppServices.of(context).server.sendPageEvent(
58+
eventTarget: widget.control.id,
59+
eventName: "submit",
60+
eventData: "");
61+
}
62+
return KeyEventResult.handled;
63+
} else {
64+
return KeyEventResult.ignored;
65+
}
66+
},
67+
);
68+
_shiftEnterfocusNode.addListener(_onShiftEnterFocusChange);
69+
_focusNode = FocusNode();
70+
_focusNode.addListener(_onFocusChange);
71+
}
72+
73+
@override
74+
void dispose() {
75+
_controller.dispose();
76+
_shiftEnterfocusNode.removeListener(_onShiftEnterFocusChange);
77+
_shiftEnterfocusNode.dispose();
78+
_focusNode.removeListener(_onFocusChange);
79+
_focusNode.dispose();
80+
super.dispose();
81+
}
82+
83+
void _onShiftEnterFocusChange() {
84+
setState(() {
85+
_focused = _shiftEnterfocusNode.hasFocus;
86+
});
87+
FletAppServices.of(context).server.sendPageEvent(
88+
eventTarget: widget.control.id,
89+
eventName: _shiftEnterfocusNode.hasFocus ? "focus" : "blur",
90+
eventData: "");
91+
}
92+
93+
void _onFocusChange() {
94+
setState(() {
95+
_focused = _focusNode.hasFocus;
96+
});
97+
FletAppServices.of(context).server.sendPageEvent(
98+
eventTarget: widget.control.id,
99+
eventName: _focusNode.hasFocus ? "focus" : "blur",
100+
eventData: "");
101+
}
102+
103+
@override
104+
Widget build(BuildContext context) {
105+
debugPrint("CupertinoTextField build: ${widget.control.id}");
106+
107+
bool autofocus = widget.control.attrBool("autofocus", false)!;
108+
bool disabled = widget.control.isDisabled || widget.parentDisabled;
109+
110+
return StoreConnector<AppState, Function>(
111+
distinct: true,
112+
converter: (store) => store.dispatch,
113+
builder: (context, dispatch) {
114+
debugPrint(
115+
"CupertinoTextField StoreConnector build: ${widget.control.id}");
116+
117+
String value = widget.control.attrs["value"] ?? "";
118+
if (_value != value) {
119+
_value = value;
120+
_controller.text = value;
121+
}
122+
123+
var prefixControls =
124+
widget.children.where((c) => c.name == "prefix" && c.isVisible);
125+
var suffixControls =
126+
widget.children.where((c) => c.name == "suffix" && c.isVisible);
127+
128+
bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
129+
bool multiline =
130+
widget.control.attrBool("multiline", false)! || shiftEnter;
131+
int minLines = widget.control.attrInt("minLines", 1)!;
132+
int? maxLines =
133+
widget.control.attrInt("maxLines", multiline ? null : 1);
134+
135+
bool readOnly = widget.control.attrBool("readOnly", false)!;
136+
bool password = widget.control.attrBool("password", false)!;
137+
bool onChange = widget.control.attrBool("onChange", false)!;
138+
139+
var cursorColor = HexColor.fromString(
140+
Theme.of(context), widget.control.attrString("cursorColor", "")!);
141+
var selectionColor = HexColor.fromString(Theme.of(context),
142+
widget.control.attrString("selectionColor", "")!);
143+
144+
int? maxLength = widget.control.attrInt("maxLength");
145+
146+
var textSize = widget.control.attrDouble("textSize");
147+
148+
var color = HexColor.fromString(
149+
Theme.of(context), widget.control.attrString("color", "")!);
150+
var focusedColor = HexColor.fromString(Theme.of(context),
151+
widget.control.attrString("focusedColor", "")!);
152+
153+
TextStyle? textStyle =
154+
parseTextStyle(Theme.of(context), widget.control, "textStyle");
155+
if (textSize != null || color != null || focusedColor != null) {
156+
textStyle = (textStyle ?? const TextStyle()).copyWith(
157+
fontSize: textSize,
158+
color: _focused ? focusedColor ?? color : color);
159+
}
160+
161+
TextCapitalization? textCapitalization = TextCapitalization.values
162+
.firstWhere(
163+
(a) =>
164+
a.name.toLowerCase() ==
165+
widget.control
166+
.attrString("capitalization", "")!
167+
.toLowerCase(),
168+
orElse: () => TextCapitalization.none);
169+
170+
FilteringTextInputFormatter? inputFilter =
171+
parseInputFilter(widget.control, "inputFilter");
172+
173+
List<TextInputFormatter>? inputFormatters = [];
174+
// add non-null input formatters
175+
if (inputFilter != null) {
176+
inputFormatters.add(inputFilter);
177+
}
178+
if (textCapitalization != TextCapitalization.none) {
179+
inputFormatters
180+
.add(TextCapitalizationFormatter(textCapitalization));
181+
}
182+
183+
TextInputType keyboardType = parseTextInputType(
184+
widget.control.attrString("keyboardType", "")!);
185+
186+
if (multiline) {
187+
keyboardType = TextInputType.multiline;
188+
}
189+
190+
TextAlign textAlign = TextAlign.values.firstWhere(
191+
((b) =>
192+
b.name ==
193+
widget.control.attrString("textAlign", "")!.toLowerCase()),
194+
orElse: () => TextAlign.start,
195+
);
196+
197+
bool autocorrect = widget.control.attrBool("autocorrect", true)!;
198+
bool enableSuggestions =
199+
widget.control.attrBool("enableSuggestions", true)!;
200+
bool smartDashesType =
201+
widget.control.attrBool("smartDashesType", true)!;
202+
bool smartQuotesType =
203+
widget.control.attrBool("smartQuotesType", true)!;
204+
205+
FocusNode focusNode = shiftEnter ? _shiftEnterfocusNode : _focusNode;
206+
207+
var focusValue = widget.control.attrString("focus");
208+
if (focusValue != null && focusValue != _lastFocusValue) {
209+
_lastFocusValue = focusValue;
210+
focusNode.requestFocus();
211+
}
212+
213+
BoxDecoration? defaultDecoration =
214+
const CupertinoTextField().decoration;
215+
var gradient =
216+
parseGradient(Theme.of(context), widget.control, "gradient");
217+
var blendMode = BlendMode.values.firstWhereOrNull((e) =>
218+
e.name.toLowerCase() ==
219+
widget.control.attrString("blendMode", "")!.toLowerCase());
220+
221+
var borderRadius = parseBorderRadius(widget.control, "borderRadius");
222+
var bgColor = HexColor.fromString(
223+
Theme.of(context), widget.control.attrString("bgColor", "")!);
224+
225+
Widget textField = CupertinoTextField(
226+
style: textStyle,
227+
placeholder: widget.control.attrString("placeholderText"),
228+
placeholderStyle: parseTextStyle(
229+
Theme.of(context), widget.control, "placeholderStyle"),
230+
autofocus: autofocus,
231+
enabled: !disabled,
232+
onSubmitted: !multiline
233+
? (_) {
234+
FletAppServices.of(context).server.sendPageEvent(
235+
eventTarget: widget.control.id,
236+
eventName: "submit",
237+
eventData: "");
238+
}
239+
: null,
240+
decoration: defaultDecoration?.copyWith(
241+
color: bgColor,
242+
gradient: gradient,
243+
backgroundBlendMode:
244+
bgColor != null || gradient != null ? blendMode : null,
245+
border:
246+
parseBorder(Theme.of(context), widget.control, "border"),
247+
borderRadius: borderRadius,
248+
boxShadow: parseBoxShadow(
249+
Theme.of(context), widget.control, "shadow")),
250+
cursorHeight: widget.control.attrDouble("cursorHeight"),
251+
showCursor: widget.control.attrBool("showCursor"),
252+
cursorWidth: widget.control.attrDouble("cursorWidth") ?? 2.0,
253+
cursorRadius: parseRadius(widget.control, "cursorRadius") ??
254+
const Radius.circular(2.0),
255+
keyboardType: keyboardType,
256+
autocorrect: autocorrect,
257+
enableSuggestions: enableSuggestions,
258+
smartDashesType: smartDashesType
259+
? SmartDashesType.enabled
260+
: SmartDashesType.disabled,
261+
smartQuotesType: smartQuotesType
262+
? SmartQuotesType.enabled
263+
: SmartQuotesType.disabled,
264+
suffixMode: parseVisibilityMode(
265+
widget.control.attrString("suffixVisibilityMode", "")!),
266+
prefixMode: parseVisibilityMode(
267+
widget.control.attrString("prefixVisibilityMode", "")!),
268+
textAlign: textAlign,
269+
minLines: minLines,
270+
maxLines: maxLines,
271+
maxLength: maxLength,
272+
prefix: prefixControls.isNotEmpty
273+
? createControl(
274+
widget.control, prefixControls.first.id, disabled)
275+
: null,
276+
suffix: suffixControls.isNotEmpty
277+
? createControl(
278+
widget.control, suffixControls.first.id, disabled)
279+
: null,
280+
readOnly: readOnly,
281+
inputFormatters:
282+
inputFormatters.isNotEmpty ? inputFormatters : null,
283+
obscureText: password && !_revealPassword,
284+
controller: _controller,
285+
focusNode: focusNode,
286+
onChanged: (String value) {
287+
//debugPrint(value);
288+
setState(() {
289+
_value = value;
290+
});
291+
List<Map<String, String>> props = [
292+
{"i": widget.control.id, "value": value}
293+
];
294+
dispatch(UpdateControlPropsAction(
295+
UpdateControlPropsPayload(props: props)));
296+
FletAppServices.of(context)
297+
.server
298+
.updateControlProps(props: props);
299+
if (onChange) {
300+
FletAppServices.of(context).server.sendPageEvent(
301+
eventTarget: widget.control.id,
302+
eventName: "change",
303+
eventData: value);
304+
}
305+
});
306+
307+
if (cursorColor != null || selectionColor != null) {
308+
textField = TextSelectionTheme(
309+
data: TextSelectionTheme.of(context).copyWith(
310+
cursorColor: cursorColor, selectionColor: selectionColor),
311+
child: textField);
312+
}
313+
314+
if (widget.control.attrInt("expand", 0)! > 0) {
315+
return constrainedControl(
316+
context, textField, widget.parent, widget.control);
317+
} else {
318+
return LayoutBuilder(
319+
builder: (BuildContext context, BoxConstraints constraints) {
320+
if (constraints.maxWidth == double.infinity &&
321+
widget.control.attrDouble("width") == null) {
322+
textField = ConstrainedBox(
323+
constraints: const BoxConstraints.tightFor(width: 300),
324+
child: textField,
325+
);
326+
}
327+
328+
return constrainedControl(
329+
context, textField, widget.parent, widget.control);
330+
},
331+
);
332+
}
333+
});
334+
}
335+
}

package/lib/src/controls/form_field.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:flutter/cupertino.dart';
12
import 'package:flutter/material.dart';
23

34
import '../models/control.dart';
@@ -137,3 +138,17 @@ InputDecoration buildInputDecoration(BuildContext context, Control control,
137138
suffixText: suffixText,
138139
suffixStyle: parseTextStyle(Theme.of(context), control, "suffixStyle"));
139140
}
141+
142+
OverlayVisibilityMode parseVisibilityMode(String type) {
143+
switch (type.toLowerCase()) {
144+
case "never":
145+
return OverlayVisibilityMode.never;
146+
case "notediting":
147+
return OverlayVisibilityMode.notEditing;
148+
case "editing":
149+
return OverlayVisibilityMode.editing;
150+
case "always":
151+
return OverlayVisibilityMode.always;
152+
}
153+
return OverlayVisibilityMode.always;
154+
}

0 commit comments

Comments
 (0)