|
| 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 | +} |
0 commit comments