Line data Source code
1 : // Copyright 2014 The Flutter Authors.
2 : // Copyright 2021 Suragch.
3 : // All rights reserved.
4 : // Use of this source code is governed by a BSD-style license that can be
5 : // found in the LICENSE file.
6 :
7 : import 'package:flutter/cupertino.dart' show CupertinoTheme;
8 : import 'package:flutter/foundation.dart';
9 : import 'package:flutter/gestures.dart';
10 : import 'package:flutter/rendering.dart';
11 : import 'package:flutter/services.dart';
12 : import 'package:flutter/widgets.dart' hide EditableTextState;
13 : import 'package:flutter/material.dart'
14 : show
15 : InputCounterWidgetBuilder,
16 : Theme,
17 : Feedback,
18 : InputDecoration,
19 : MaterialLocalizations,
20 : ThemeData,
21 : debugCheckHasMaterial,
22 : debugCheckHasMaterialLocalizations,
23 : TextSelectionThemeData,
24 : TextSelectionTheme,
25 : iOSHorizontalOffset,
26 : MaterialStateProperty,
27 : MaterialStateMouseCursor,
28 : MaterialState;
29 : import 'package:mongol/src/base/mongol_text_align.dart';
30 :
31 : import 'alignment.dart';
32 : import 'mongol_editable_text.dart';
33 : import 'mongol_input_decorator.dart';
34 : import 'text_selection/mongol_text_selection.dart';
35 : import 'text_selection/mongol_text_selection_controls.dart';
36 :
37 : class _TextFieldSelectionGestureDetectorBuilder
38 : extends MongolTextSelectionGestureDetectorBuilder {
39 1 : _TextFieldSelectionGestureDetectorBuilder({
40 : required _TextFieldState state,
41 : }) : _state = state,
42 1 : super(delegate: state);
43 :
44 : final _TextFieldState _state;
45 :
46 0 : @override
47 : void onForcePressStart(ForcePressDetails details) {
48 0 : super.onForcePressStart(details);
49 0 : if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
50 0 : editableText.showToolbar();
51 : }
52 : }
53 :
54 0 : @override
55 : void onForcePressEnd(ForcePressDetails details) {
56 : // Not required.
57 : }
58 :
59 1 : @override
60 : void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
61 2 : if (delegate.selectionEnabled) {
62 4 : switch (Theme.of(_state.context).platform) {
63 1 : case TargetPlatform.iOS:
64 1 : case TargetPlatform.macOS:
65 0 : renderEditable.selectPositionAt(
66 0 : from: details.globalPosition,
67 : cause: SelectionChangedCause.longPress,
68 : );
69 : break;
70 1 : case TargetPlatform.android:
71 0 : case TargetPlatform.fuchsia:
72 0 : case TargetPlatform.linux:
73 0 : case TargetPlatform.windows:
74 2 : renderEditable.selectWordsInRange(
75 3 : from: details.globalPosition - details.offsetFromOrigin,
76 1 : to: details.globalPosition,
77 : cause: SelectionChangedCause.longPress,
78 : );
79 : break;
80 : }
81 : }
82 : }
83 :
84 1 : @override
85 : void onSingleTapUp(TapUpDetails details) {
86 2 : editableText.hideToolbar();
87 2 : if (delegate.selectionEnabled) {
88 4 : switch (Theme.of(_state.context).platform) {
89 1 : case TargetPlatform.iOS:
90 1 : case TargetPlatform.macOS:
91 : // Disabling this because of issue #12
92 : // https://github.com/suragch/mongol/issues/12
93 : // switch (details.kind) {
94 : // case PointerDeviceKind.mouse:
95 : // case PointerDeviceKind.stylus:
96 : // case PointerDeviceKind.invertedStylus:
97 : // // Precise devices should place the cursor at a precise position.
98 : // renderEditable.selectPosition(cause: SelectionChangedCause.tap);
99 : // break;
100 : // case PointerDeviceKind.touch:
101 : // case PointerDeviceKind.unknown:
102 : // // On macOS/iOS/iPadOS a touch tap places the cursor at the edge
103 : // // of the word.
104 : // renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
105 : // break;
106 : // }
107 : // break;
108 1 : case TargetPlatform.android:
109 1 : case TargetPlatform.fuchsia:
110 1 : case TargetPlatform.linux:
111 1 : case TargetPlatform.windows:
112 2 : renderEditable.selectPosition(cause: SelectionChangedCause.tap);
113 : break;
114 : }
115 : }
116 2 : _state._requestKeyboard();
117 3 : if (_state.widget.onTap != null) {
118 4 : _state.widget.onTap!();
119 : }
120 : }
121 :
122 1 : @override
123 : void onSingleLongTapStart(LongPressStartDetails details) {
124 2 : if (delegate.selectionEnabled) {
125 4 : switch (Theme.of(_state.context).platform) {
126 1 : case TargetPlatform.iOS:
127 1 : case TargetPlatform.macOS:
128 0 : renderEditable.selectPositionAt(
129 0 : from: details.globalPosition,
130 : cause: SelectionChangedCause.longPress,
131 : );
132 : break;
133 1 : case TargetPlatform.android:
134 0 : case TargetPlatform.fuchsia:
135 0 : case TargetPlatform.linux:
136 0 : case TargetPlatform.windows:
137 2 : renderEditable.selectWord(cause: SelectionChangedCause.longPress);
138 3 : Feedback.forLongPress(_state.context);
139 : break;
140 : }
141 : }
142 : }
143 : }
144 :
145 : /// A material design text field for vertical Mongolian script.
146 : ///
147 : /// A text field lets the user enter text, either with hardware keyboard or with
148 : /// an onscreen keyboard.
149 : ///
150 : /// The text field calls the [onChanged] callback whenever the user changes the
151 : /// text in the field. If the user indicates that they are done typing in the
152 : /// field (e.g., by pressing a button on the soft keyboard), the text field
153 : /// calls the [onSubmitted] callback.
154 : ///
155 : /// To control the text that is displayed in the text field, use the
156 : /// [controller]. For example, to set the initial value of the text field, use
157 : /// a [controller] that already contains some text. The [controller] can also
158 : /// control the selection and composing region (and to observe changes to the
159 : /// text, selection, and composing region).
160 : ///
161 : /// By default, a text field has a [decoration] that draws a divider to the
162 : /// right of the text field. You can use the [decoration] property to control
163 : /// the decoration, for example by adding a label or an icon. If you set the
164 : /// [decoration] property to null, the decoration will be removed entirely,
165 : /// including the extra padding introduced by the decoration to save space for
166 : /// the labels.
167 : ///
168 : /// If [decoration] is non-null (which is the default), the text field requires
169 : /// one of its ancestors to be a [Material] widget.
170 : ///
171 : /// To integrate the [MongolTextField] into a [Form] with other [FormField] widgets,
172 : /// consider using [MongolTextFormField].
173 : ///
174 : /// Remember to call [TextEditingController.dispose] of the [TextEditingController]
175 : /// when it is no longer needed. This will ensure we discard any resources used
176 : /// by the object.
177 : ///
178 : /// {@tool snippet}
179 : /// This example shows how to create a [MongolTextField] that will obscure input. The
180 : /// [InputDecoration] surrounds the field in a border using [OutlineInputBorder]
181 : /// and adds a label.
182 : ///
183 : /// 
184 : ///
185 : /// ```dart
186 : /// MongolTextField(
187 : /// obscureText: true,
188 : /// decoration: InputDecoration(
189 : /// border: OutlineInputBorder(),
190 : /// labelText: 'Password',
191 : /// ),
192 : /// )
193 : /// ```
194 : /// {@end-tool}
195 : ///
196 : /// ## Reading values
197 : ///
198 : /// A common way to read a value from a MongolTextField is to use the [onSubmitted]
199 : /// callback. This callback is applied to the text field's current value when
200 : /// the user finishes editing.
201 : ///
202 : /// {@tool dartpad --template=stateful_widget_material}
203 : ///
204 : /// This sample shows how to get a value from a MongolTextField via the [onSubmitted]
205 : /// callback.
206 : ///
207 : /// ```dart
208 : /// // TODO: update the example with Mongol alert and Mongol button
209 : ///
210 : /// late TextEditingController _controller;
211 : ///
212 : /// void initState() {
213 : /// super.initState();
214 : /// _controller = TextEditingController();
215 : /// }
216 : ///
217 : /// void dispose() {
218 : /// _controller.dispose();
219 : /// super.dispose();
220 : /// }
221 : ///
222 : /// Widget build(BuildContext context) {
223 : /// return Scaffold(
224 : /// body: Center(
225 : /// child: MongolTextField(
226 : /// controller: _controller,
227 : /// onSubmitted: (String value) async {
228 : /// await showDialog<void>(
229 : /// context: context,
230 : /// builder: (BuildContext context) {
231 : /// return AlertDialog(
232 : /// title: const Text('Thanks!'),
233 : /// content: Text ('You typed "$value", which has length ${value.characters.length}.'),
234 : /// actions: <Widget>[
235 : /// TextButton(
236 : /// onPressed: () { Navigator.pop(context); },
237 : /// child: const Text('OK'),
238 : /// ),
239 : /// ],
240 : /// );
241 : /// },
242 : /// );
243 : /// },
244 : /// ),
245 : /// ),
246 : /// );
247 : /// }
248 : /// ```
249 : /// {@end-tool}
250 : ///
251 : /// For most applications the [onSubmitted] callback will be sufficient for
252 : /// reacting to user input.
253 : ///
254 : /// The [onEditingComplete] callback also runs when the user finishes editing.
255 : /// It's different from [onSubmitted] because it has a default value which
256 : /// updates the text controller and yields the keyboard focus. Applications that
257 : /// require different behavior can override the default [onEditingComplete]
258 : /// callback.
259 : ///
260 : /// Keep in mind you can also always read the current string from a MongolTextField's
261 : /// [TextEditingController] using [TextEditingController.text].
262 : ///
263 : /// ## Handling emojis and other complex characters
264 : /// {@macro flutter.widgets.EditableText.onChanged}
265 : ///
266 : /// In the live Dartpad example above, try typing the emoji 👨👩👦
267 : /// into the field and submitting. Because the example code measures the length
268 : /// with `value.characters.length`, the emoji is correctly counted as a single
269 : /// character.
270 : ///
271 : /// See also:
272 : ///
273 : /// * [MongolTextFormField], which integrates with the [Form] widget.
274 : /// * [InputDecorator], which shows the labels and other visual elements that
275 : /// surround the actual text editing widget.
276 : /// * [MongolEditableText], which is the raw text editing control at the heart of a
277 : /// [MongolTextField]. The [MongolEditableText] widget is rarely used directly unless
278 : /// you are implementing an entirely different design language, such as
279 : /// Cupertino.
280 : class MongolTextField extends StatefulWidget {
281 : /// Creates a Material Design text field for vertical Mongolian text.
282 : ///
283 : /// If [decoration] is non-null (which is the default), the text field requires
284 : /// one of its ancestors to be a [Material] widget.
285 : ///
286 : /// To remove the decoration entirely (including the extra padding introduced
287 : /// by the decoration to save space for the labels), set the [decoration] to
288 : /// null.
289 : ///
290 : /// The [maxLines] property can be set to null to remove the restriction on
291 : /// the number of lines. By default, it is one, meaning this is a single-line
292 : /// text field. [maxLines] must not be zero.
293 : ///
294 : /// The [maxLength] property is set to null by default, which means the
295 : /// number of characters allowed in the text field is not restricted. If
296 : /// [maxLength] is set a character counter will be displayed to the right of the
297 : /// field showing how many characters have been entered. If the value is
298 : /// set to a positive integer it will also display the maximum allowed
299 : /// number of characters to be entered. If the value is set to
300 : /// [TextField.noMaxLength] then only the current length is displayed.
301 : ///
302 : /// After [maxLength] characters have been input, additional input
303 : /// is ignored, unless [maxLengthEnforcement] is set to
304 : /// [MaxLengthEnforcement.none].
305 : /// The text field enforces the length with a [LengthLimitingTextInputFormatter],
306 : /// which is evaluated after the supplied [inputFormatters], if any.
307 : /// The [maxLength] value must be either null or greater than zero.
308 : ///
309 : /// The text cursor is not shown if [showCursor] is false or if [showCursor]
310 : /// is null (the default) and [readOnly] is true.
311 : ///
312 : /// See also:
313 : ///
314 : /// * [maxLength], which discusses the precise meaning of "number of
315 : /// characters" and how it may differ from the intuitive meaning.
316 2 : const MongolTextField({
317 : Key? key,
318 : this.controller,
319 : this.focusNode,
320 : this.decoration = const InputDecoration(),
321 : TextInputType? keyboardType,
322 : this.textInputAction,
323 : this.style,
324 : this.textAlign = MongolTextAlign.top,
325 : this.textAlignHorizontal,
326 : this.readOnly = false,
327 : ToolbarOptions? toolbarOptions,
328 : this.showCursor,
329 : this.autofocus = false,
330 : this.obscuringCharacter = '•',
331 : this.obscureText = false,
332 : this.autocorrect = true,
333 : this.enableSuggestions = true,
334 : this.maxLines = 1,
335 : this.minLines,
336 : this.expands = false,
337 : this.maxLength,
338 : this.onChanged,
339 : this.onEditingComplete,
340 : this.onSubmitted,
341 : this.onAppPrivateCommand,
342 : this.inputFormatters,
343 : this.enabled,
344 : this.cursorHeight = 2.0,
345 : this.cursorWidth,
346 : this.cursorRadius,
347 : this.cursorColor,
348 : this.keyboardAppearance,
349 : this.scrollPadding = const EdgeInsets.all(20.0),
350 : this.dragStartBehavior = DragStartBehavior.start,
351 : this.enableInteractiveSelection = true,
352 : this.selectionControls,
353 : this.onTap,
354 : this.mouseCursor,
355 : this.buildCounter,
356 : this.scrollController,
357 : this.scrollPhysics,
358 : this.autofillHints,
359 : this.restorationId,
360 2 : }) : assert(obscuringCharacter.length == 1),
361 1 : assert(maxLines == null || maxLines > 0),
362 0 : assert(minLines == null || minLines > 0),
363 : assert(
364 0 : (maxLines == null) || (minLines == null) || (maxLines >= minLines),
365 : "minLines can't be greater than maxLines",
366 : ),
367 : assert(
368 0 : !expands || (maxLines == null && minLines == null),
369 : 'minLines and maxLines must be null when expands is true.',
370 : ),
371 1 : assert(!obscureText || maxLines == 1,
372 : 'Obscured fields cannot be multiline.'),
373 0 : assert(maxLength == null ||
374 1 : maxLength == MongolTextField.noMaxLength ||
375 1 : maxLength > 0),
376 : // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set.
377 : assert(
378 0 : !identical(textInputAction, TextInputAction.newline) ||
379 0 : maxLines == 1 ||
380 : !identical(keyboardType, TextInputType.text),
381 : 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.'),
382 : keyboardType = keyboardType ??
383 1 : (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
384 : toolbarOptions = toolbarOptions ??
385 : (obscureText
386 : ? const ToolbarOptions(
387 : selectAll: true,
388 : paste: true,
389 : )
390 : : const ToolbarOptions(
391 : copy: true,
392 : cut: true,
393 : selectAll: true,
394 : paste: true,
395 : )),
396 1 : super(key: key);
397 :
398 : /// Controls the text being edited.
399 : ///
400 : /// If null, this widget will create its own [TextEditingController].
401 : final TextEditingController? controller;
402 :
403 : /// Defines the keyboard focus for this widget.
404 : ///
405 : /// The [focusNode] is a long-lived object that's typically managed by a
406 : /// [StatefulWidget] parent. See [FocusNode] for more information.
407 : ///
408 : /// To give the keyboard focus to this widget, provide a [focusNode] and then
409 : /// use the current [FocusScope] to request the focus:
410 : ///
411 : /// ```dart
412 : /// FocusScope.of(context).requestFocus(myFocusNode);
413 : /// ```
414 : ///
415 : /// This happens automatically when the widget is tapped.
416 : ///
417 : /// To be notified when the widget gains or loses the focus, add a listener
418 : /// to the [focusNode]:
419 : ///
420 : /// ```dart
421 : /// focusNode.addListener(() { print(myFocusNode.hasFocus); });
422 : /// ```
423 : ///
424 : /// If null, this widget will create its own [FocusNode].
425 : ///
426 : /// ## Keyboard
427 : ///
428 : /// Requesting the focus will typically cause the keyboard to be shown
429 : /// if it's not showing already.
430 : ///
431 : /// On Android, the user can hide the keyboard - without changing the focus -
432 : /// with the system back button. They can restore the keyboard's visibility
433 : /// by tapping on a text field. The user might hide the keyboard and
434 : /// switch to a physical keyboard, or they might just need to get it
435 : /// out of the way for a moment, to expose something it's
436 : /// obscuring. In this case requesting the focus again will not
437 : /// cause the focus to change, and will not make the keyboard visible.
438 : ///
439 : /// This widget builds a [MongolEditableText] and will ensure that the keyboard is
440 : /// showing when it is tapped by calling [MongolEditableTextState.requestKeyboard()].
441 : final FocusNode? focusNode;
442 :
443 : /// The decoration to show around the text field.
444 : ///
445 : /// By default, draws a vertical line to the right of the text field but can be
446 : /// configured to show an icon, label, hint text, and error text.
447 : ///
448 : /// Specify null to remove the decoration entirely (including the
449 : /// extra padding introduced by the decoration to save space for the labels).
450 : final InputDecoration? decoration;
451 :
452 : /// The type of keyboard to use for editing the text.
453 : ///
454 : /// Defaults to [TextInputType.text] if [maxLines] is one and
455 : /// [TextInputType.multiline] otherwise.
456 : final TextInputType keyboardType;
457 :
458 : /// The type of action button to use for the keyboard.
459 : ///
460 : /// Defaults to [TextInputAction.newline] if [keyboardType] is
461 : /// [TextInputType.multiline] and [TextInputAction.done] otherwise.
462 : final TextInputAction? textInputAction;
463 :
464 : /// The style to use for the text being edited.
465 : ///
466 : /// This text style is also used as the base style for the [decoration].
467 : ///
468 : /// If null, defaults to the `subtitle1` text style from the current [Theme].
469 : final TextStyle? style;
470 :
471 : /// How the text should be aligned vertically.
472 : ///
473 : /// Defaults to [MongolTextAlign.top].
474 : final MongolTextAlign textAlign;
475 :
476 : /// The horizontal alignment of verical Mongolian text within an input box.
477 : final TextAlignHorizontal? textAlignHorizontal;
478 :
479 : /// Whether this text field should focus itself if nothing else is already
480 : /// focused.
481 : ///
482 : /// If true, the keyboard will open as soon as this text field obtains focus.
483 : /// Otherwise, the keyboard is only shown after the user taps the text field.
484 : ///
485 : /// Defaults to false.
486 : final bool autofocus;
487 :
488 : /// Character used for obscuring text if [obscureText] is true.
489 : ///
490 : /// Must be only a single character.
491 : ///
492 : /// Defaults to the character U+2022 BULLET (•).
493 : final String obscuringCharacter;
494 :
495 : /// Whether to hide the text being edited (e.g., for passwords).
496 : ///
497 : /// When this is set to true, all the characters in the text field are
498 : /// replaced by [obscuringCharacter].
499 : ///
500 : /// Defaults to false.
501 : final bool obscureText;
502 :
503 : /// Whether to enable autocorrection.
504 : ///
505 : /// Defaults to true.
506 : final bool autocorrect;
507 :
508 : /// Whether to show input suggestions as the user types.
509 : ///
510 : /// This flag only affects Android. On iOS, suggestions are tied directly to
511 : /// [autocorrect], so that suggestions are only shown when [autocorrect] is
512 : /// true. On Android autocorrection and suggestion are controlled separately.
513 : ///
514 : /// Defaults to true.
515 : ///
516 : /// See also:
517 : ///
518 : /// * <https://developer.android.com/reference/android/text/InputType.html#TYPE_TEXT_FLAG_NO_SUGGESTIONS>
519 : final bool enableSuggestions;
520 :
521 : /// The maximum number of lines for the text to span, wrapping if necessary.
522 : ///
523 : /// If this is 1 (the default), the text will not wrap, but will scroll
524 : /// vertically instead.
525 : ///
526 : /// If this is null, there is no limit to the number of lines, and the text
527 : /// container will start with enough horizontal space for one line and
528 : /// automatically grow to accommodate additional lines as they are entered.
529 : ///
530 : /// If this is not null, the value must be greater than zero, and it will lock
531 : /// the input to the given number of lines and take up enough vertical space
532 : /// to accommodate that number of lines. Setting [minLines] as well allows the
533 : /// input to grow between the indicated range.
534 : ///
535 : /// The full set of behaviors possible with [minLines] and [maxLines] are as
536 : /// follows. These examples apply equally to `MongolTextField`,
537 : /// `MongolTextFormField`, and `MongolEditableText`.
538 : ///
539 : /// Input that occupies a single line and scrolls vertically as needed.
540 : /// ```dart
541 : /// MongolTextField()
542 : /// ```
543 : ///
544 : /// Input whose width grows from one line up to as many lines as needed for
545 : /// the text that was entered. If a width limit is imposed by its parent, it
546 : /// will scroll horizontally when its width reaches that limit.
547 : /// ```dart
548 : /// MongolTextField(maxLines: null)
549 : /// ```
550 : ///
551 : /// The input's width is large enough for the given number of lines. If
552 : /// additional lines are entered the input scrolls horizontally.
553 : /// ```dart
554 : /// MongolTextField(maxLines: 2)
555 : /// ```
556 : ///
557 : /// Input whose width grows with content between a min and max. An infinite
558 : /// max is possible with `maxLines: null`.
559 : /// ```dart
560 : /// MongolTextField(minLines: 2, maxLines: 4)
561 : /// ```
562 : final int? maxLines;
563 :
564 : /// The minimum number of lines to occupy when the content spans fewer lines.
565 : ///
566 : /// If this is null (default), text container starts with enough horizontal space
567 : /// for one line and grows to accommodate additional lines as they are entered.
568 : ///
569 : /// This can be used in combination with [maxLines] for a varying set of behaviors.
570 : ///
571 : /// If the value is set, it must be greater than zero. If the value is greater
572 : /// than 1, [maxLines] should also be set to either null or greater than
573 : /// this value.
574 : ///
575 : /// When [maxLines] is set as well, the width will grow between the indicated
576 : /// range of lines. When [maxLines] is null, it will grow as wide as needed,
577 : /// starting from [minLines].
578 : ///
579 : /// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
580 : /// These apply equally to `MongolTextField`, `MongolTextFormField`,
581 : /// and `MongolEditableText`.
582 : ///
583 : /// Input that always occupies at least 2 lines and has an infinite max.
584 : /// Expands horizontally as needed.
585 : /// ```dart
586 : /// MongolTextField(minLines: 2)
587 : /// ```
588 : ///
589 : /// Input whose width starts from 2 lines and grows up to 4 lines at which
590 : /// point the width limit is reached. If additional lines are entered it will
591 : /// scroll horizontally.
592 : /// ```dart
593 : /// MongolTextField(minLines:2, maxLines: 4)
594 : /// ```
595 : ///
596 : /// See the examples in [maxLines] for the complete picture of how [maxLines]
597 : /// and [minLines] interact to produce various behaviors.
598 : ///
599 : /// Defaults to null.
600 : final int? minLines;
601 :
602 : /// Whether this widget's width will be sized to fill its parent.
603 : ///
604 : /// If set to true and wrapped in a parent widget like [Expanded] or
605 : /// [SizedBox], the input will expand to fill the parent.
606 : ///
607 : /// [maxLines] and [minLines] must both be null when this is set to true,
608 : /// otherwise an error is thrown.
609 : ///
610 : /// Defaults to false.
611 : ///
612 : /// See the examples in [maxLines] for the complete picture of how [maxLines],
613 : /// [minLines], and [expands] interact to produce various behaviors.
614 : ///
615 : /// Input that matches the width of its parent:
616 : /// ```dart
617 : /// Expanded(
618 : /// child: MongolTextField(maxLines: null, expands: true),
619 : /// )
620 : /// ```
621 : final bool expands;
622 :
623 : /// Whether the text can be changed.
624 : ///
625 : /// When this is set to true, the text cannot be modified
626 : /// by any shortcut or keyboard operation. The text is still selectable.
627 : ///
628 : /// Defaults to false.
629 : final bool readOnly;
630 :
631 : /// Configuration of toolbar options.
632 : ///
633 : /// If not set, select all and paste will default to be enabled. Copy and cut
634 : /// will be disabled if [obscureText] is true. If [readOnly] is true,
635 : /// paste and cut will be disabled regardless.
636 : final ToolbarOptions toolbarOptions;
637 :
638 : /// Whether to show cursor.
639 : ///
640 : /// The cursor refers to the blinking caret when the [MongolEditableText] is
641 : /// focused.
642 : ///
643 : /// See also:
644 : ///
645 : /// * [showSelectionHandles], which controls the visibility of the selection
646 : /// handles.
647 : final bool? showCursor;
648 :
649 : /// If [maxLength] is set to this value, only the "current input length"
650 : /// part of the character counter is shown.
651 : static const int noMaxLength = -1;
652 :
653 : /// The maximum number of characters (Unicode scalar values) to allow in the
654 : /// text field.
655 : ///
656 : /// TODO: add support for maxLengthEnforcement.
657 : ///
658 : /// If set, a character counter will be displayed to the right of the
659 : /// field showing how many characters have been entered. If set to a number
660 : /// greater than 0, it will also display the maximum number allowed. If set
661 : /// to [MongolTextField.noMaxLength] then only the current character count
662 : /// is displayed.
663 : ///
664 : /// After [maxLength] characters have been input, additional input
665 : /// is ignored, unless [maxLengthEnforcement] is set to
666 : /// [MaxLengthEnforcement.none].
667 : ///
668 : /// The text field enforces the length with a [LengthLimitingTextInputFormatter],
669 : /// which is evaluated after the supplied [inputFormatters], if any.
670 : ///
671 : /// This value must be either null, [MongolTextField.noMaxLength], or greater than 0.
672 : /// If null (the default) then there is no limit to the number of characters
673 : /// that can be entered. If set to [MongolTextField.noMaxLength], then no limit will
674 : /// be enforced, but the number of characters entered will still be displayed.
675 : ///
676 : /// Whitespace characters (e.g. newline, space, tab) are included in the
677 : /// character count.
678 : ///
679 : /// If [maxLengthEnforcement] is
680 : /// [MaxLengthEnforcement.none], then more than [maxLength]
681 : /// characters may be entered, but the error counter and divider will switch
682 : /// to the [decoration]'s [InputDecoration.errorStyle] when the limit is
683 : /// exceeded.
684 : ///
685 : /// ## Characters
686 : ///
687 : /// For a specific definition of what is considered a character, see the
688 : /// [characters](https://pub.dev/packages/characters) package on Pub, which is
689 : /// what Flutter uses to delineate characters. In general, even complex
690 : /// characters like surrogate pairs and extended grapheme clusters are
691 : /// correctly interpreted by Flutter as each being a single user-perceived
692 : /// character.
693 : ///
694 : /// For instance, the character "ö" can be represented as '\u{006F}\u{0308}',
695 : /// which is the letter "o" followed by a composed diaeresis "¨", or it can
696 : /// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN
697 : /// SMALL LETTER O WITH DIAERESIS". It will be counted as a single character
698 : /// in both cases.
699 : ///
700 : /// Similarly, some emoji are represented by multiple scalar values. The
701 : /// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽"is counted as
702 : /// a single character, even though it is a combination of two Unicode scalar
703 : /// values, '\u{1F44D}\u{1F3FD}'.
704 : final int? maxLength;
705 :
706 : /// Called when the user initiates a change to the MongolTextField's
707 : /// value: when they have inserted or deleted text.
708 : ///
709 : /// This callback doesn't run when the MongolTextField's text is changed
710 : /// programmatically, via the MongolTextField's [controller]. Typically it
711 : /// isn't necessary to be notified of such changes, since they're
712 : /// initiated by the app itself.
713 : ///
714 : /// To be notified of all changes to the MongolTextField's text, cursor,
715 : /// and selection, one can add a listener to its [controller] with
716 : /// [TextEditingController.addListener].
717 : ///
718 : /// {@tool dartpad --template=stateful_widget_material}
719 : ///
720 : /// This example shows how onChanged could be used to check the MongolTextField's
721 : /// current value each time the user inserts or deletes a character.
722 : ///
723 : /// ```dart
724 : /// // TODO: test this snippet to make sure it works
725 : ///
726 : /// final TextEditingController _controller = TextEditingController();
727 : ///
728 : /// void dispose() {
729 : /// _controller.dispose();
730 : /// super.dispose();
731 : /// }
732 : ///
733 : /// Widget build(BuildContext context) {
734 : /// return Scaffold(
735 : /// body: Row(
736 : /// mainAxisAlignment: MainAxisAlignment.center,
737 : /// children: <Widget>[
738 : /// const MongolText('What number comes next in the sequence?'),
739 : /// const MongolText('1, 1, 2, 3, 5, 8...?'),
740 : /// MongolTextField(
741 : /// controller: _controller,
742 : /// onChanged: (String value) async {
743 : /// if (value != '13') {
744 : /// return;
745 : /// }
746 : /// await showDialog<void>(
747 : /// context: context,
748 : /// builder: (BuildContext context) {
749 : /// return MongolAlertDialog(
750 : /// title: const Text('That is correct!'),
751 : /// content: Text ('13 is the right answer.'),
752 : /// actions: <Widget>[
753 : /// MongolTextButton(
754 : /// onPressed: () { Navigator.pop(context); },
755 : /// child: const Text('OK'),
756 : /// ),
757 : /// ],
758 : /// );
759 : /// },
760 : /// );
761 : /// },
762 : /// ),
763 : /// ],
764 : /// ),
765 : /// );
766 : /// }
767 : /// ```
768 : /// {@end-tool}
769 : ///
770 : /// ## Handling emojis and other complex characters
771 : ///
772 : /// It's important to always use
773 : /// [characters](https://pub.dev/packages/characters) when dealing with user
774 : /// input text that may contain complex characters. This will ensure that
775 : /// extended grapheme clusters and surrogate pairs are treated as single
776 : /// characters, as they appear to the user.
777 : ///
778 : /// For example, when finding the length of some user input, use
779 : /// `string.characters.length`. Do NOT use `string.length` or even
780 : /// `string.runes.length`. For the complex character "👨👩👦", this
781 : /// appears to the user as a single character, and `string.characters.length`
782 : /// intuitively returns 1. On the other hand, `string.length` returns 8, and
783 : /// `string.runes.length` returns 5!
784 : ///
785 : /// See also:
786 : ///
787 : /// * [inputFormatters], which are called before [onChanged]
788 : /// runs and can validate and change ("format") the input value.
789 : /// * [onEditingComplete], [onSubmitted]:
790 : /// which are more specialized input change notifications.
791 : final ValueChanged<String>? onChanged;
792 :
793 : /// Called when the user submits editable content (e.g., user presses the "done"
794 : /// button on the keyboard).
795 : ///
796 : /// The default implementation of [onEditingComplete] executes 2 different
797 : /// behaviors based on the situation:
798 : ///
799 : /// - When a completion action is pressed, such as "done", "go", "send", or
800 : /// "search", the user's content is submitted to the [controller] and then
801 : /// focus is given up.
802 : ///
803 : /// - When a non-completion action is pressed, such as "next" or "previous",
804 : /// the user's content is submitted to the [controller], but focus is not
805 : /// given up because developers may want to immediately move focus to
806 : /// another input widget within [onSubmitted].
807 : ///
808 : /// Providing [onEditingComplete] prevents the aforementioned default behavior.
809 : final VoidCallback? onEditingComplete;
810 :
811 : /// Called when the user indicates that they are done editing the text in the
812 : /// field.
813 : ///
814 : /// See also:
815 : ///
816 : /// * [TextInputAction.next] and [TextInputAction.previous], which
817 : /// automatically shift the focus to the next/previous focusable item when
818 : /// the user is done editing.
819 : final ValueChanged<String>? onSubmitted;
820 :
821 : /// This is used to receive a private command from the input method.
822 : ///
823 : /// Called when the result of [TextInputClient.performPrivateCommand] is
824 : /// received.
825 : ///
826 : /// This can be used to provide domain-specific features that are only known
827 : /// between certain input methods and their clients.
828 : ///
829 : /// See also:
830 : /// * [https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand(java.lang.String,%20android.os.Bundle)],
831 : /// which is the Android documentation for performPrivateCommand, used to
832 : /// send a command from the input method.
833 : /// * [https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand],
834 : /// which is the Android documentation for sendAppPrivateCommand, used to
835 : /// send a command to the input method.
836 : final AppPrivateCommandCallback? onAppPrivateCommand;
837 :
838 : /// Optional input validation and formatting overrides.
839 : ///
840 : /// Formatters are run in the provided order when the text input changes. When
841 : /// this parameter changes, the new formatters will not be applied until the
842 : /// next time the user inserts or deletes text.
843 : final List<TextInputFormatter>? inputFormatters;
844 :
845 : /// If false the text field is "disabled": it ignores taps and its
846 : /// [decoration] is rendered in grey.
847 : ///
848 : /// If non-null this property overrides the [decoration]'s
849 : /// [InputDecoration.enabled] property.
850 : final bool? enabled;
851 :
852 : /// How wide the cursor will be.
853 : ///
854 : /// If this property is null, [MongolRenderEditable.preferredLineWidth] will
855 : /// be used.
856 : final double? cursorWidth;
857 :
858 : /// How thick the cursor will be.
859 : ///
860 : /// Defaults to 2.0.
861 : ///
862 : /// The cursor will draw above the text. The cursor height will extend
863 : /// down from the boundary between characters. This corresponds to extending
864 : /// downstream relative to the selected position. Negative values may be used
865 : /// to reverse this behavior.
866 : final double cursorHeight;
867 :
868 : /// How rounded the corners of the cursor should be.
869 : ///
870 : /// By default, the cursor has no radius.
871 : final Radius? cursorRadius;
872 :
873 : /// The color of the cursor.
874 : ///
875 : /// The cursor indicates the current location of text insertion point in
876 : /// the field.
877 : ///
878 : /// If this is null it will default to the ambient
879 : /// [TextSelectionThemeData.cursorColor]. If that is null, and the
880 : /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
881 : /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
882 : /// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
883 : final Color? cursorColor;
884 :
885 : /// The appearance of the keyboard.
886 : ///
887 : /// This setting is only honored on iOS devices.
888 : ///
889 : /// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
890 : final Brightness? keyboardAppearance;
891 :
892 : /// Configures padding to edges surrounding a [Scrollable] when the
893 : /// MongolTextField scrolls into view.
894 : ///
895 : /// When this widget receives focus and is not completely visible (for
896 : /// example scrolled partially off the screen or overlapped by the keyboard)
897 : /// then it will attempt to make itself visible by scrolling a surrounding
898 : /// [Scrollable], if one is present. This value controls how far from the
899 : /// edges of a [Scrollable] the MongolTextField will be positioned after the
900 : /// scroll.
901 : ///
902 : /// Defaults to EdgeInsets.all(20.0).
903 : final EdgeInsets scrollPadding;
904 :
905 : /// Whether to enable user interface affordances for changing the
906 : /// text selection.
907 : ///
908 : /// For example, setting this to true will enable features such as
909 : /// long-pressing the MongolTextField to select text and show the
910 : /// cut/copy/paste menu, and tapping to move the text caret.
911 : ///
912 : /// When this is false, the text selection cannot be adjusted by
913 : /// the user, text cannot be copied, and the user cannot paste into
914 : /// the text field from the clipboard.
915 : final bool enableInteractiveSelection;
916 :
917 : /// Optional delegate for building the text selection handles and toolbar.
918 : ///
919 : /// The [MongolEditableText] widget used on its own will not trigger the display
920 : /// of the selection toolbar by itself. The toolbar is shown by calling
921 : /// [EditableTextState.showToolbar] in response to an appropriate user event.
922 : ///
923 : /// See also:
924 : ///
925 : /// * [MongolTextField], a Material Design themed wrapper of
926 : /// [MongolEditableText], which shows the selection toolbar upon
927 : /// appropriate user events based on the user's platform set in
928 : /// [ThemeData.platform].
929 : ///
930 : final TextSelectionControls? selectionControls;
931 :
932 : /// Determines the way that drag start behavior is handled.
933 : ///
934 : /// If set to [DragStartBehavior.start], scrolling drag behavior will
935 : /// begin upon the detection of a drag gesture. If set to
936 : /// [DragStartBehavior.down] it will begin when a down event is first detected.
937 : ///
938 : /// In general, setting this to [DragStartBehavior.start] will make drag
939 : /// animation smoother and setting it to [DragStartBehavior.down] will make
940 : /// drag behavior feel slightly more reactive.
941 : ///
942 : /// By default, the drag start behavior is [DragStartBehavior.start].
943 : ///
944 : /// See also:
945 : ///
946 : /// * [DragGestureRecognizer.dragStartBehavior], which gives an example for
947 : /// the different behaviors.
948 : final DragStartBehavior dragStartBehavior;
949 :
950 : /// Same as [enableInteractiveSelection].
951 : ///
952 : /// This getter exists primarily for consistency with
953 : /// [MongolRenderEditable.selectionEnabled].
954 2 : bool get selectionEnabled => enableInteractiveSelection;
955 :
956 : /// Called for each distinct tap except for every second tap of a double tap.
957 : ///
958 : /// The text field builds a [GestureDetector] to handle input events like tap,
959 : /// to trigger focus requests, to move the caret, adjust the selection, etc.
960 : /// Handling some of those events by wrapping the text field with a competing
961 : /// GestureDetector is problematic.
962 : ///
963 : /// To unconditionally handle taps, without interfering with the text field's
964 : /// internal gesture detector, provide this callback.
965 : ///
966 : /// If the text field is created with [enabled] false, taps will not be
967 : /// recognized.
968 : ///
969 : /// To be notified when the text field gains or loses the focus, provide a
970 : /// [focusNode] and add a listener to that.
971 : ///
972 : /// To listen to arbitrary pointer events without competing with the
973 : /// text field's internal gesture detector, use a [Listener].
974 : final GestureTapCallback? onTap;
975 :
976 : /// The cursor for a mouse pointer when it enters or is hovering over the
977 : /// widget.
978 : ///
979 : /// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
980 : /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s:
981 : ///
982 : /// * [MaterialState.error].
983 : /// * [MaterialState.hovered].
984 : /// * [MaterialState.focused].
985 : /// * [MaterialState.disabled].
986 : ///
987 : /// If this property is null, [MaterialStateMouseCursor.textable] will be used.
988 : ///
989 : /// The [mouseCursor] is the only property of [MongolTextField] that controls the
990 : /// appearance of the mouse pointer. All other properties related to "cursor"
991 : /// stand for the text cursor, which is usually a blinking horizontal line at
992 : /// the editing position.
993 : final MouseCursor? mouseCursor;
994 :
995 : /// Callback that generates a custom [InputDecoration.counter] widget.
996 : ///
997 : /// See [InputCounterWidgetBuilder] for an explanation of the passed in
998 : /// arguments. The returned widget will be placed below the line in place of
999 : /// the default widget built when [InputDecoration.counterText] is specified.
1000 : ///
1001 : /// The returned widget will be wrapped in a [Semantics] widget for
1002 : /// accessibility, but it also needs to be accessible itself. For example,
1003 : /// if returning a MongolText widget, set the [MongolText.semanticsLabel] property.
1004 : ///
1005 : /// {@tool snippet}
1006 : /// ```dart
1007 : /// Widget counter(
1008 : /// BuildContext context,
1009 : /// {
1010 : /// required int currentLength,
1011 : /// required int? maxLength,
1012 : /// required bool isFocused,
1013 : /// }
1014 : /// ) {
1015 : /// return MongolText(
1016 : /// '$currentLength of $maxLength characters',
1017 : /// semanticsLabel: 'character count',
1018 : /// );
1019 : /// }
1020 : /// ```
1021 : /// {@end-tool}
1022 : ///
1023 : /// If buildCounter returns null, then no counter and no Semantics widget will
1024 : /// be created at all.
1025 : final InputCounterWidgetBuilder? buildCounter;
1026 :
1027 : /// The [ScrollPhysics] to use when horizontally scrolling the input.
1028 : ///
1029 : /// If not specified, it will behave according to the current platform.
1030 : ///
1031 : /// See [Scrollable.physics].
1032 : final ScrollPhysics? scrollPhysics;
1033 :
1034 : /// The [ScrollController] to use when horizontally scrolling the input.
1035 : ///
1036 : /// If null, it will instantiate a new ScrollController.
1037 : ///
1038 : /// See [Scrollable.controller].
1039 : final ScrollController? scrollController;
1040 :
1041 : /// A list of strings that helps the autofill service identify the type of this
1042 : /// text input.
1043 : ///
1044 : /// When set to null or empty, this text input will not send its autofill
1045 : /// information to the platform, preventing it from participating in
1046 : /// autofills triggered by a different [AutofillClient], even if they're in the
1047 : /// same [AutofillScope]. Additionally, on Android and web, setting this to
1048 : /// null or empty will disable autofill for this text field.
1049 : ///
1050 : /// The minimum platform SDK version that supports Autofill is API level 26
1051 : /// for Android, and iOS 10.0 for iOS.
1052 : ///
1053 : /// ### Setting up iOS autofill:
1054 : ///
1055 : /// To provide the best user experience and ensure your app fully supports
1056 : /// password autofill on iOS, follow these steps:
1057 : ///
1058 : /// * Set up your iOS app's
1059 : /// [associated domains](https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app).
1060 : /// * Some autofill hints only work with specific [keyboardType]s. For example,
1061 : /// [AutofillHints.name] requires [TextInputType.name] and [AutofillHints.email]
1062 : /// works only with [TextInputType.emailAddress]. Make sure the input field has a
1063 : /// compatible [keyboardType]. Empirically, [TextInputType.name] works well
1064 : /// with many autofill hints that are predefined on iOS.
1065 : ///
1066 : /// ### Troubleshooting Autofill
1067 : ///
1068 : /// Autofill service providers rely heavily on [autofillHints]. Make sure the
1069 : /// entries in [autofillHints] are supported by the autofill service currently
1070 : /// in use (the name of the service can typically be found in your mobile
1071 : /// device's system settings).
1072 : ///
1073 : /// #### Autofill UI refuses to show up when I tap on the text field
1074 : ///
1075 : /// Check the device's system settings and make sure autofill is turned on,
1076 : /// and there're available credentials stored in the autofill service.
1077 : ///
1078 : /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill
1079 : /// Passwords", and add new passwords for testing by pressing the top right
1080 : /// "+" button. Use an arbitrary "website" if you don't have associated
1081 : /// domains set up for your app. As long as there's at least one password
1082 : /// stored, you should be able to see a key-shaped icon in the quick type
1083 : /// bar on the software keyboard, when a password related field is focused.
1084 : ///
1085 : /// * iOS contact information autofill: iOS seems to pull contact info from
1086 : /// the Apple ID currently associated with the device. Go to Settings ->
1087 : /// Apple ID (usually the first entry, or "Sign in to your iPhone" if you
1088 : /// haven't set up one on the device), and fill out the relevant fields. If
1089 : /// you wish to test more contact info types, try adding them in Contacts ->
1090 : /// My Card.
1091 : ///
1092 : /// * Android autofill: Go to Settings -> System -> Languages & input ->
1093 : /// Autofill service. Enable the autofill service of your choice, and make
1094 : /// sure there're available credentials associated with your app.
1095 : ///
1096 : /// #### I called `TextInput.finishAutofillContext` but the autofill save
1097 : /// prompt isn't showing
1098 : ///
1099 : /// * iOS: iOS may not show a prompt or any other visual indication when it
1100 : /// saves user password. Go to Settings -> Password and check if your new
1101 : /// password is saved. Neither saving password nor auto-generating strong
1102 : /// password works without properly setting up associated domains in your
1103 : /// app. To set up associated domains, follow the instructions in
1104 : /// <https://developer.apple.com/documentation/safariservices/supporting_associated_domains_in_your_app>.
1105 : ///
1106 : /// For the best results, hint strings need to be understood by the platform's
1107 : /// autofill service. The common values of hint strings can be found in
1108 : /// [AutofillHints], as well as their availability on different platforms.
1109 : ///
1110 : /// If an autofillable input field needs to use a custom hint that translates to
1111 : /// different strings on different platforms, the easiest way to achieve that
1112 : /// is to return different hint strings based on the value of
1113 : /// [defaultTargetPlatform].
1114 : ///
1115 : /// Each hint in the list, if not ignored, will be translated to the platform's
1116 : /// autofill hint type understood by its autofill services:
1117 : ///
1118 : /// * On iOS, only the first hint in the list is accounted for. The hint will
1119 : /// be translated to a
1120 : /// [UITextContentType](https://developer.apple.com/documentation/uikit/uitextcontenttype).
1121 : ///
1122 : /// * On Android, all hints in the list are translated to Android hint strings.
1123 : ///
1124 : /// * On web, only the first hint is accounted for and will be translated to
1125 : /// an "autocomplete" string.
1126 : ///
1127 : /// Providing an autofill hint that is predefined on the platform does not
1128 : /// automatically grant the input field eligibility for autofill. Ultimately,
1129 : /// it comes down to the autofill service currently in charge to determine
1130 : /// whether an input field is suitable for autofill and what the autofill
1131 : /// candidates are.
1132 : ///
1133 : /// See also:
1134 : ///
1135 : /// * [AutofillHints], a list of autofill hint strings that is predefined on at
1136 : /// least one platform.
1137 : ///
1138 : /// * [UITextContentType](https://developer.apple.com/documentation/uikit/uitextcontenttype),
1139 : /// the iOS equivalent.
1140 : ///
1141 : /// * Android [autofillHints](https://developer.android.com/reference/android/view/View#setAutofillHints(java.lang.String...)),
1142 : /// the Android equivalent.
1143 : ///
1144 : /// * The [autocomplete](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) attribute,
1145 : /// the web equivalent.
1146 : final Iterable<String>? autofillHints;
1147 :
1148 : /// Restoration ID to save and restore the state of the text field.
1149 : ///
1150 : /// If non-null, the text field will persist and restore its current scroll
1151 : /// offset and - if no [controller] has been provided - the content of the
1152 : /// text field. If a [controller] has been provided, it is the responsibility
1153 : /// of the owner of that controller to persist and restore it, e.g. by using
1154 : /// a [RestorableTextEditingController].
1155 : ///
1156 : /// The state of this widget is persisted in a [RestorationBucket] claimed
1157 : /// from the surrounding [RestorationScope] using the provided restoration ID.
1158 : ///
1159 : /// See also:
1160 : ///
1161 : /// * [RestorationManager], which explains how state restoration works in
1162 : /// Flutter.
1163 : final String? restorationId;
1164 :
1165 1 : @override
1166 1 : _TextFieldState createState() => _TextFieldState();
1167 :
1168 1 : @override
1169 : void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1170 1 : super.debugFillProperties(properties);
1171 2 : properties.add(DiagnosticsProperty<TextEditingController>(
1172 1 : 'controller', controller,
1173 : defaultValue: null));
1174 3 : properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
1175 : defaultValue: null));
1176 : properties
1177 3 : .add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
1178 2 : properties.add(DiagnosticsProperty<InputDecoration>(
1179 1 : 'decoration', decoration,
1180 : defaultValue: const InputDecoration()));
1181 2 : properties.add(DiagnosticsProperty<TextInputType>(
1182 1 : 'keyboardType', keyboardType,
1183 : defaultValue: TextInputType.text));
1184 1 : properties.add(
1185 2 : DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
1186 1 : properties.add(
1187 2 : DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
1188 2 : properties.add(DiagnosticsProperty<String>(
1189 1 : 'obscuringCharacter', obscuringCharacter,
1190 : defaultValue: '•'));
1191 3 : properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
1192 : defaultValue: false));
1193 3 : properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
1194 : defaultValue: true));
1195 2 : properties.add(DiagnosticsProperty<bool>(
1196 1 : 'enableSuggestions', enableSuggestions,
1197 : defaultValue: true));
1198 3 : properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
1199 3 : properties.add(IntProperty('minLines', minLines, defaultValue: null));
1200 1 : properties.add(
1201 2 : DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
1202 3 : properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
1203 2 : properties.add(EnumProperty<TextInputAction>(
1204 1 : 'textInputAction', textInputAction,
1205 : defaultValue: null));
1206 3 : properties.add(EnumProperty<MongolTextAlign>('textAlign', textAlign,
1207 : defaultValue: MongolTextAlign.top));
1208 2 : properties.add(DiagnosticsProperty<TextAlignHorizontal>(
1209 1 : 'textAlignHorizontal', textAlignHorizontal,
1210 : defaultValue: null));
1211 : properties
1212 3 : .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: null));
1213 : properties
1214 3 : .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: 2.0));
1215 3 : properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
1216 : defaultValue: null));
1217 : properties
1218 3 : .add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
1219 2 : properties.add(DiagnosticsProperty<Brightness>(
1220 1 : 'keyboardAppearance', keyboardAppearance,
1221 : defaultValue: null));
1222 2 : properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
1223 1 : 'scrollPadding', scrollPadding,
1224 : defaultValue: const EdgeInsets.all(20.0)));
1225 2 : properties.add(FlagProperty('selectionEnabled',
1226 1 : value: selectionEnabled,
1227 : defaultValue: true,
1228 : ifFalse: 'selection disabled'));
1229 2 : properties.add(DiagnosticsProperty<TextSelectionControls>(
1230 1 : 'selectionControls', selectionControls,
1231 : defaultValue: null));
1232 2 : properties.add(DiagnosticsProperty<ScrollController>(
1233 1 : 'scrollController', scrollController,
1234 : defaultValue: null));
1235 2 : properties.add(DiagnosticsProperty<ScrollPhysics>(
1236 1 : 'scrollPhysics', scrollPhysics,
1237 : defaultValue: null));
1238 : }
1239 : }
1240 :
1241 : class _TextFieldState extends State<MongolTextField>
1242 : with RestorationMixin
1243 : implements MongolTextSelectionGestureDetectorBuilderDelegate {
1244 : RestorableTextEditingController? _controller;
1245 1 : TextEditingController get _effectiveController =>
1246 4 : widget.controller ?? _controller!.value;
1247 :
1248 : FocusNode? _focusNode;
1249 1 : FocusNode get _effectiveFocusNode =>
1250 4 : widget.focusNode ?? (_focusNode ??= FocusNode());
1251 :
1252 : bool _isHovering = false;
1253 :
1254 0 : bool get needsCounter =>
1255 0 : widget.maxLength != null &&
1256 0 : widget.decoration != null &&
1257 0 : widget.decoration!.counterText == null;
1258 :
1259 : bool _showSelectionHandles = false;
1260 :
1261 : late _TextFieldSelectionGestureDetectorBuilder
1262 : _selectionGestureDetectorBuilder;
1263 :
1264 : // API for MongolTextSelectionGestureDetectorBuilderDelegate.
1265 : @override
1266 : late bool forcePressEnabled;
1267 :
1268 : @override
1269 : final GlobalKey<MongolEditableTextState> editableTextKey =
1270 : GlobalKey<MongolEditableTextState>();
1271 :
1272 1 : @override
1273 2 : bool get selectionEnabled => widget.selectionEnabled;
1274 : // End of API for MongolTextSelectionGestureDetectorBuilderDelegate.
1275 :
1276 6 : bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true;
1277 :
1278 6 : int get _currentLength => _effectiveController.value.text.characters.length;
1279 :
1280 1 : bool get _hasIntrinsicError =>
1281 2 : widget.maxLength != null &&
1282 3 : widget.maxLength! > 0 &&
1283 8 : _effectiveController.value.text.characters.length > widget.maxLength!;
1284 :
1285 1 : bool get _hasError =>
1286 4 : widget.decoration?.errorText != null || _hasIntrinsicError;
1287 :
1288 1 : InputDecoration _getEffectiveDecoration() {
1289 2 : final localizations = MaterialLocalizations.of(context);
1290 2 : final themeData = Theme.of(context);
1291 2 : final effectiveDecoration = (widget.decoration ?? const InputDecoration())
1292 2 : .applyDefaults(themeData.inputDecorationTheme)
1293 1 : .copyWith(
1294 1 : enabled: _isEnabled,
1295 5 : hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines,
1296 : );
1297 :
1298 : // No need to build anything if counter or counterText were given directly.
1299 1 : if (effectiveDecoration.counter != null ||
1300 1 : effectiveDecoration.counterText != null) return effectiveDecoration;
1301 :
1302 : // If buildCounter was provided, use it to generate a counter widget.
1303 : Widget? counter;
1304 1 : final currentLength = _currentLength;
1305 1 : if (effectiveDecoration.counter == null &&
1306 1 : effectiveDecoration.counterText == null &&
1307 2 : widget.buildCounter != null) {
1308 2 : final isFocused = _effectiveFocusNode.hasFocus;
1309 3 : final builtCounter = widget.buildCounter!(
1310 1 : context,
1311 : currentLength: currentLength,
1312 2 : maxLength: widget.maxLength,
1313 : isFocused: isFocused,
1314 : );
1315 : // If buildCounter returns null, don't add a counter widget to the field.
1316 : if (builtCounter != null) {
1317 0 : counter = Semantics(
1318 : container: true,
1319 : liveRegion: isFocused,
1320 : child: builtCounter,
1321 : );
1322 : }
1323 1 : return effectiveDecoration.copyWith(counter: counter);
1324 : }
1325 :
1326 2 : if (widget.maxLength == null) {
1327 : return effectiveDecoration; // No counter widget
1328 : }
1329 :
1330 1 : var counterText = '$currentLength';
1331 : var semanticCounterText = '';
1332 :
1333 : // Handle a real maxLength (positive number)
1334 3 : if (widget.maxLength! > 0) {
1335 : // Show the maxLength in the counter
1336 4 : counterText += '/${widget.maxLength}';
1337 : final remaining =
1338 6 : (widget.maxLength! - currentLength).clamp(0, widget.maxLength!);
1339 : semanticCounterText =
1340 1 : localizations.remainingTextFieldCharacterCount(remaining);
1341 : }
1342 :
1343 1 : if (_hasIntrinsicError) {
1344 0 : return effectiveDecoration.copyWith(
1345 0 : errorText: effectiveDecoration.errorText ?? '',
1346 0 : counterStyle: effectiveDecoration.errorStyle ??
1347 0 : themeData.textTheme.caption!.copyWith(color: themeData.errorColor),
1348 : counterText: counterText,
1349 : semanticCounterText: semanticCounterText,
1350 : );
1351 : }
1352 :
1353 1 : return effectiveDecoration.copyWith(
1354 : counterText: counterText,
1355 : semanticCounterText: semanticCounterText,
1356 : );
1357 : }
1358 :
1359 1 : @override
1360 : void initState() {
1361 1 : super.initState();
1362 1 : _selectionGestureDetectorBuilder =
1363 1 : _TextFieldSelectionGestureDetectorBuilder(state: this);
1364 2 : if (widget.controller == null) {
1365 1 : _createLocalController();
1366 : }
1367 3 : _effectiveFocusNode.canRequestFocus = _isEnabled;
1368 : }
1369 :
1370 1 : bool get _canRequestFocus {
1371 3 : final mode = MediaQuery.maybeOf(context)?.navigationMode ??
1372 : NavigationMode.traditional;
1373 : switch (mode) {
1374 1 : case NavigationMode.traditional:
1375 1 : return _isEnabled;
1376 1 : case NavigationMode.directional:
1377 : return true;
1378 : }
1379 : }
1380 :
1381 1 : @override
1382 : void didChangeDependencies() {
1383 1 : super.didChangeDependencies();
1384 3 : _effectiveFocusNode.canRequestFocus = _canRequestFocus;
1385 : }
1386 :
1387 1 : @override
1388 : void didUpdateWidget(MongolTextField oldWidget) {
1389 1 : super.didUpdateWidget(oldWidget);
1390 3 : if (widget.controller == null && oldWidget.controller != null) {
1391 3 : _createLocalController(oldWidget.controller!.value);
1392 3 : } else if (widget.controller != null && oldWidget.controller == null) {
1393 2 : unregisterFromRestoration(_controller!);
1394 2 : _controller!.dispose();
1395 1 : _controller = null;
1396 : }
1397 3 : _effectiveFocusNode.canRequestFocus = _canRequestFocus;
1398 2 : if (_effectiveFocusNode.hasFocus &&
1399 4 : widget.readOnly != oldWidget.readOnly &&
1400 1 : _isEnabled) {
1401 3 : if (_effectiveController.selection.isCollapsed) {
1402 3 : _showSelectionHandles = !widget.readOnly;
1403 : }
1404 : }
1405 : }
1406 :
1407 1 : @override
1408 : void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
1409 1 : if (_controller != null) {
1410 1 : _registerController();
1411 : }
1412 : }
1413 :
1414 1 : void _registerController() {
1415 1 : assert(_controller != null);
1416 2 : registerForRestoration(_controller!, 'controller');
1417 : }
1418 :
1419 1 : void _createLocalController([TextEditingValue? value]) {
1420 1 : assert(_controller == null);
1421 1 : _controller = value == null
1422 1 : ? RestorableTextEditingController()
1423 1 : : RestorableTextEditingController.fromValue(value);
1424 1 : if (!restorePending) {
1425 1 : _registerController();
1426 : }
1427 : }
1428 :
1429 1 : @override
1430 2 : String? get restorationId => widget.restorationId;
1431 :
1432 1 : @override
1433 : void dispose() {
1434 2 : _focusNode?.dispose();
1435 2 : _controller?.dispose();
1436 1 : super.dispose();
1437 : }
1438 :
1439 3 : MongolEditableTextState? get _editableText => editableTextKey.currentState;
1440 :
1441 1 : void _requestKeyboard() {
1442 2 : _editableText?.requestKeyboard();
1443 : }
1444 :
1445 1 : bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
1446 : // When the text field is activated by something that doesn't trigger the
1447 : // selection overlay, we shouldn't show the handles either.
1448 2 : if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) {
1449 : return false;
1450 : }
1451 :
1452 1 : if (cause == SelectionChangedCause.keyboard) return false;
1453 :
1454 5 : if (widget.readOnly && _effectiveController.selection.isCollapsed) {
1455 : return false;
1456 : }
1457 :
1458 1 : if (!_isEnabled) return false;
1459 :
1460 1 : if (cause == SelectionChangedCause.longPress) return true;
1461 :
1462 3 : if (_effectiveController.text.isNotEmpty) return true;
1463 :
1464 : return false;
1465 : }
1466 :
1467 1 : void _handleSelectionChanged(
1468 : TextSelection selection, SelectionChangedCause? cause) {
1469 1 : final willShowSelectionHandles = _shouldShowSelectionHandles(cause);
1470 2 : if (willShowSelectionHandles != _showSelectionHandles) {
1471 2 : setState(() {
1472 1 : _showSelectionHandles = willShowSelectionHandles;
1473 : });
1474 : }
1475 :
1476 3 : switch (Theme.of(context).platform) {
1477 1 : case TargetPlatform.iOS:
1478 1 : case TargetPlatform.macOS:
1479 1 : if (cause == SelectionChangedCause.longPress) {
1480 0 : _editableText?.bringIntoView(selection.base);
1481 : }
1482 : return;
1483 1 : case TargetPlatform.android:
1484 1 : case TargetPlatform.fuchsia:
1485 1 : case TargetPlatform.linux:
1486 1 : case TargetPlatform.windows:
1487 : // Do nothing.
1488 : }
1489 : }
1490 :
1491 : /// Toggle the toolbar when a selection handle is tapped.
1492 1 : void _handleSelectionHandleTapped() {
1493 3 : if (_effectiveController.selection.isCollapsed) {
1494 2 : _editableText!.toggleToolbar();
1495 : }
1496 : }
1497 :
1498 1 : void _handleHover(bool hovering) {
1499 2 : if (hovering != _isHovering) {
1500 2 : setState(() {
1501 1 : _isHovering = hovering;
1502 : });
1503 : }
1504 : }
1505 :
1506 1 : @override
1507 : Widget build(BuildContext context) {
1508 1 : assert(debugCheckHasMaterial(context));
1509 1 : assert(debugCheckHasMaterialLocalizations(context));
1510 : assert(
1511 3 : !(widget.style != null &&
1512 4 : widget.style!.inherit == false &&
1513 3 : (widget.style!.fontSize == null ||
1514 3 : widget.style!.textBaseline == null)),
1515 : 'inherit false style must supply fontSize and textBaseline',
1516 : );
1517 :
1518 1 : final theme = Theme.of(context);
1519 1 : final selectionTheme = TextSelectionTheme.of(context);
1520 5 : final style = theme.textTheme.subtitle1!.merge(widget.style);
1521 : final keyboardAppearance =
1522 3 : widget.keyboardAppearance ?? theme.primaryColorBrightness;
1523 1 : final controller = _effectiveController;
1524 1 : final focusNode = _effectiveFocusNode;
1525 1 : final formatters = <TextInputFormatter>[
1526 2 : ...?widget.inputFormatters,
1527 : ];
1528 :
1529 2 : var textSelectionControls = widget.selectionControls;
1530 : final bool cursorOpacityAnimates;
1531 : Offset? cursorOffset;
1532 2 : var cursorColor = widget.cursorColor;
1533 : final Color selectionColor;
1534 2 : var cursorRadius = widget.cursorRadius;
1535 :
1536 1 : switch (theme.platform) {
1537 1 : case TargetPlatform.iOS:
1538 1 : final cupertinoTheme = CupertinoTheme.of(context);
1539 1 : forcePressEnabled = true;
1540 1 : textSelectionControls ??= mongolTextSelectionControls;
1541 : cursorOpacityAnimates = true;
1542 : cursorColor ??=
1543 2 : selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
1544 1 : selectionColor = selectionTheme.selectionColor ??
1545 2 : cupertinoTheme.primaryColor.withOpacity(0.40);
1546 : cursorRadius ??= const Radius.circular(2.0);
1547 1 : cursorOffset = Offset(
1548 3 : iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
1549 : break;
1550 :
1551 1 : case TargetPlatform.macOS:
1552 1 : final cupertinoTheme = CupertinoTheme.of(context);
1553 1 : forcePressEnabled = false;
1554 1 : textSelectionControls ??= mongolTextSelectionControls;
1555 : cursorOpacityAnimates = true;
1556 : cursorColor ??=
1557 2 : selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
1558 1 : selectionColor = selectionTheme.selectionColor ??
1559 2 : cupertinoTheme.primaryColor.withOpacity(0.40);
1560 : cursorRadius ??= const Radius.circular(2.0);
1561 1 : cursorOffset = Offset(
1562 3 : iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
1563 : break;
1564 :
1565 1 : case TargetPlatform.android:
1566 1 : case TargetPlatform.fuchsia:
1567 1 : forcePressEnabled = false;
1568 1 : textSelectionControls ??= mongolTextSelectionControls;
1569 : cursorOpacityAnimates = false;
1570 3 : cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
1571 1 : selectionColor = selectionTheme.selectionColor ??
1572 3 : theme.colorScheme.primary.withOpacity(0.40);
1573 : break;
1574 :
1575 1 : case TargetPlatform.linux:
1576 1 : case TargetPlatform.windows:
1577 1 : forcePressEnabled = false;
1578 1 : textSelectionControls ??= mongolTextSelectionControls;
1579 : cursorOpacityAnimates = false;
1580 3 : cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
1581 1 : selectionColor = selectionTheme.selectionColor ??
1582 3 : theme.colorScheme.primary.withOpacity(0.40);
1583 : break;
1584 : }
1585 :
1586 1 : Widget child = RepaintBoundary(
1587 1 : child: UnmanagedRestorationScope(
1588 1 : bucket: bucket,
1589 1 : child: MongolEditableText(
1590 1 : key: editableTextKey,
1591 3 : readOnly: widget.readOnly || !_isEnabled,
1592 2 : toolbarOptions: widget.toolbarOptions,
1593 2 : showCursor: widget.showCursor,
1594 1 : showSelectionHandles: _showSelectionHandles,
1595 : controller: controller,
1596 : focusNode: focusNode,
1597 2 : keyboardType: widget.keyboardType,
1598 2 : textInputAction: widget.textInputAction,
1599 : style: style,
1600 2 : textAlign: widget.textAlign,
1601 2 : autofocus: widget.autofocus,
1602 2 : obscuringCharacter: widget.obscuringCharacter,
1603 2 : obscureText: widget.obscureText,
1604 2 : autocorrect: widget.autocorrect,
1605 2 : enableSuggestions: widget.enableSuggestions,
1606 2 : maxLines: widget.maxLines,
1607 2 : minLines: widget.minLines,
1608 2 : expands: widget.expands,
1609 : selectionColor: selectionColor,
1610 : selectionControls:
1611 2 : widget.selectionEnabled ? textSelectionControls : null,
1612 2 : onChanged: widget.onChanged,
1613 1 : onSelectionChanged: _handleSelectionChanged,
1614 2 : onEditingComplete: widget.onEditingComplete,
1615 2 : onSubmitted: widget.onSubmitted,
1616 2 : onAppPrivateCommand: widget.onAppPrivateCommand,
1617 1 : onSelectionHandleTapped: _handleSelectionHandleTapped,
1618 : inputFormatters: formatters,
1619 : rendererIgnoresPointer: true,
1620 : mouseCursor:
1621 : MouseCursor.defer, // MongolTextField will handle the cursor
1622 2 : cursorWidth: widget.cursorWidth,
1623 2 : cursorHeight: widget.cursorHeight,
1624 : cursorRadius: cursorRadius,
1625 : cursorColor: cursorColor,
1626 : cursorOpacityAnimates: cursorOpacityAnimates,
1627 : cursorOffset: cursorOffset,
1628 2 : scrollPadding: widget.scrollPadding,
1629 : keyboardAppearance: keyboardAppearance,
1630 2 : enableInteractiveSelection: widget.enableInteractiveSelection,
1631 2 : dragStartBehavior: widget.dragStartBehavior,
1632 2 : scrollController: widget.scrollController,
1633 2 : scrollPhysics: widget.scrollPhysics,
1634 2 : autofillHints: widget.autofillHints,
1635 : restorationId: 'editable',
1636 : ),
1637 : ),
1638 : );
1639 :
1640 2 : if (widget.decoration != null) {
1641 1 : child = AnimatedBuilder(
1642 2 : animation: Listenable.merge(<Listenable>[focusNode, controller]),
1643 1 : builder: (BuildContext context, Widget? child) {
1644 1 : return MongolInputDecorator(
1645 1 : decoration: _getEffectiveDecoration(),
1646 2 : baseStyle: widget.style,
1647 2 : textAlign: widget.textAlign,
1648 2 : textAlignHorizontal: widget.textAlignHorizontal,
1649 1 : isHovering: _isHovering,
1650 1 : isFocused: focusNode.hasFocus,
1651 3 : isEmpty: controller.value.text.isEmpty,
1652 2 : expands: widget.expands,
1653 : child: child,
1654 : );
1655 : },
1656 : child: child,
1657 : );
1658 : }
1659 1 : final effectiveMouseCursor = MaterialStateProperty.resolveAs<MouseCursor>(
1660 2 : widget.mouseCursor ?? MaterialStateMouseCursor.textable,
1661 : <MaterialState>{
1662 2 : if (!_isEnabled) MaterialState.disabled,
1663 2 : if (_isHovering) MaterialState.hovered,
1664 2 : if (focusNode.hasFocus) MaterialState.focused,
1665 2 : if (_hasError) MaterialState.error,
1666 : },
1667 : );
1668 :
1669 : final int? semanticsMaxValueLength;
1670 5 : if (widget.maxLength != null && widget.maxLength! > 0) {
1671 2 : semanticsMaxValueLength = widget.maxLength;
1672 : } else {
1673 : semanticsMaxValueLength = null;
1674 : }
1675 :
1676 1 : return MouseRegion(
1677 : cursor: effectiveMouseCursor,
1678 2 : onEnter: (PointerEnterEvent event) => _handleHover(true),
1679 0 : onExit: (PointerExitEvent event) => _handleHover(false),
1680 1 : child: IgnorePointer(
1681 1 : ignoring: !_isEnabled,
1682 1 : child: AnimatedBuilder(
1683 : animation: controller, // changes the _currentLength
1684 1 : builder: (BuildContext context, Widget? child) {
1685 1 : return Semantics(
1686 : maxValueLength: semanticsMaxValueLength,
1687 1 : currentValueLength: _currentLength,
1688 2 : onTap: widget.readOnly
1689 : ? null
1690 0 : : () {
1691 0 : if (!_effectiveController.selection.isValid) {
1692 0 : _effectiveController.selection =
1693 0 : TextSelection.collapsed(
1694 0 : offset: _effectiveController.text.length);
1695 : }
1696 0 : _requestKeyboard();
1697 : },
1698 : child: child,
1699 : );
1700 : },
1701 2 : child: _selectionGestureDetectorBuilder.buildGestureDetector(
1702 : behavior: HitTestBehavior.translucent,
1703 : child: child,
1704 : ),
1705 : ),
1706 : ),
1707 : );
1708 : }
1709 : }
|