diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3749b0..be30421 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,74 @@
-## 7.0.0-non-null-safety
+# 11.0.1
+
+* fix issue on ios after flutter version 3.7.0. #191 #198
+
+# 11.0.0
+
+* Migrate to 3.7.0
+
+# 10.2.0
+
+* Add TextInputBindingMixin to prevent system keyboard show.
+* Add No SystemKeyboard demo
+
+# 10.1.1
+
+* Fix issue selection not right #172
+
+# 10.1.0
+
+* Migrate to 3.0.0
+* Support Scribble Handwriting for iPads
+
+# 10.0.1
+
+* Public ExtendedTextFieldState and add bringIntoView method to support jump to caret when insert text with TextEditingController
+
+# 10.0.0
+
+* Migrate to 2.10.0.
+* Add shouldShowSelectionHandles and textSelectionGestureDetectorBuilder call back to define the behavior of handles and toolbar.
+* Shortcut support for web and desktop.
+
+# 9.0.3
+
+* Fix hittest is not right #131
+
+# 9.0.2
+
+* Fix selectionWidthStyle and selectionHeightStyle are not working.
+
+## 9.0.1
+
+* Support to use keyboard move cursor for SpecialInlineSpan. #135
+* Fix issue that backspace delete two chars. #141
+
+## 9.0.0
+
+* Migrate to 2.8
+
+## 8.0.0
+
+* Add [SpecialTextSpan.mouseCursor], [SpecialTextSpan.onEnter] and [SpecialTextSpan.onExit].
+* merge code from 2.2.0
+
+## 7.0.1
+
+* Fix issue that composing is not updated.#122
+
+## 7.0.0
* Breaking change: [SpecialText.getContent] is not include endflag now.(please check if you call getContent and your endflag length is more than 1)
* Fix demo manualDelete error #120
-## 6.0.0-non-null-safety
-* non-null-safety
+## 6.0.1
+
+* Fix issue that toolbar is not shown when double tap
+* Fix throw exception when selectWordAtOffset
+
+## 6.0.0
+
+* Support null-safety
## 5.0.4
diff --git a/README-ZH.md b/README-ZH.md
index 55b4eba..dfa81da 100644
--- a/README-ZH.md
+++ b/README-ZH.md
@@ -2,9 +2,12 @@
[![pub package](https://img.shields.io/pub/v/extended_text_field.svg)](https://pub.dartlang.org/packages/extended_text_field) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/issues)
+文档语言: [English](README.md) | 中文简体
+
官方输入框的扩展组件,支持图片,@某人,自定义文字背景。也支持自定义菜单和选择器。
-文档语言: [English](README.md) | [中文简体](README-ZH.md)
+[ExtendedTextField 在线 Demo](https://fluttercandies.github.io/extended_text_field/)
+
- [extended_text_field](#extended_text_field)
- [限制](#限制)
@@ -16,6 +19,10 @@
- [缓存图片](#缓存图片)
- [文本选择控制器](#文本选择控制器)
- [WidgetSpan](#widgetspan)
+ - [阻止系统键盘](#阻止系统键盘)
+ - [TextInputBindingMixin](#textinputbindingmixin)
+ - [TextInputFocusNode](#textinputfocusnode)
+ - [CustomKeyboard](#customkeyboard)
- [☕️Buy me a coffee](#️buy-me-a-coffee)
## 限制
@@ -508,6 +515,140 @@ class EmailText extends SpecialText {
}
```
+## 阻止系统键盘
+
+我们不需要代码侵入到 [ExtendedTextField] 或者 [TextField] 当中, 就可以阻止系统键盘弹出,
+
+### TextInputBindingMixin
+
+我们通过阻止 Flutter Framework 发送 `TextInput.show` 到 Flutter 引擎来阻止系统键盘弹出
+
+你可以直接使用 [TextInputBinding].
+
+``` dart
+void main() {
+ TextInputBinding();
+ runApp(const MyApp());
+}
+```
+
+或者你如果有其他的 `binding`,你可以这样。
+
+``` dart
+ class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
+ }
+
+ void main() {
+ YourBinding();
+ runApp(const MyApp());
+ }
+```
+
+或者你需要重载 `ignoreTextInputShow` 方法,你可以这样。
+
+``` dart
+ class YourBinding extends TextInputBinding {
+ @override
+ // ignore: unnecessary_overrides
+ bool ignoreTextInputShow() {
+ // you can override it base on your case
+ // if NoKeyboardFocusNode is not enough
+ return super.ignoreTextInputShow();
+ }
+ }
+
+ void main() {
+ YourBinding();
+ runApp(const MyApp());
+ }
+```
+
+### TextInputFocusNode
+
+把 [TextInputFocusNode] 传递给 [ExtendedTextField] 或者 [TextField]。
+
+
+``` dart
+final TextInputFocusNode _focusNode = TextInputFocusNode();
+
+ @override
+ Widget build(BuildContext context) {
+ return ExtendedTextField(
+ // request keyboard if need
+ focusNode: _focusNode..debugLabel = 'ExtendedTextField',
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ // request keyboard if need
+ focusNode: _focusNode..debugLabel = 'CustomTextField',
+ );
+ }
+```
+
+我们通过当前的 `FocusNode` 是否是 [TextInputFocusNode],来决定是否阻止系统键盘弹出的。
+
+``` dart
+ final FocusNode? focus = FocusManager.instance.primaryFocus;
+ if (focus != null &&
+ focus is TextInputFocusNode &&
+ focus.ignoreSystemKeyboardShow) {
+ return true;
+ }
+```
+### CustomKeyboard
+
+你可以通过当前焦点的变化的时候,来显示或者隐藏自定义的键盘。
+
+当你的自定义键盘可以关闭而不让焦点失去,你应该在 [ExtendedTextField] 或者 [TextField]
+的 `onTap` 事件中,再次判断键盘是否显示。
+
+``` dart
+ @override
+ void initState() {
+ super.initState();
+ _focusNode.addListener(_handleFocusChanged);
+ }
+
+ void _onTextFiledTap() {
+ if (_bottomSheetController == null) {
+ _handleFocusChanged();
+ }
+ }
+
+ void _handleFocusChanged() {
+ if (_focusNode.hasFocus) {
+ // just demo, you can define your custom keyboard as you want
+ _bottomSheetController = showBottomSheet(
+ context: FocusManager.instance.primaryFocus!.context!,
+ // set false, if don't want to drag to close custom keyboard
+ enableDrag: true,
+ builder: (BuildContext b) {
+ // your custom keyboard
+ return Container();
+ });
+ // maybe drag close
+ _bottomSheetController?.closed.whenComplete(() {
+ _bottomSheetController = null;
+ });
+ } else {
+ _bottomSheetController?.close();
+ _bottomSheetController = null;
+ }
+ }
+
+ @override
+ void dispose() {
+ _focusNode.removeListener(_handleFocusChanged);
+ super.dispose();
+ }
+```
+
+
+查看 [完整的例子](https://github.com/fluttercandies/extended_text_field/tree/master/example/lib/pages/simple/no_keyboard.dart)
+
## ☕️Buy me a coffee
![img](http://zmtzawqlp.gitee.io/my_images/images/qrcode.png)
diff --git a/README.md b/README.md
index 70e1544..e236754 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,11 @@
[![pub package](https://img.shields.io/pub/v/extended_text_field.svg)](https://pub.dartlang.org/packages/extended_text_field) [![GitHub stars](https://img.shields.io/github/stars/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/stargazers) [![GitHub forks](https://img.shields.io/github/forks/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/network) [![GitHub license](https://img.shields.io/github/license/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/issues/fluttercandies/extended_text_field)](https://github.com/fluttercandies/extended_text_field/issues)
+Language: English | [中文简体](README-ZH.md)
+
Extended official text field to build special text like inline image, @somebody, custom background etc quickly.It also support to build custom seleciton toolbar and handles.
-Language: [English](README.md) | [中文简体](README-ZH.md)
+[Web demo for ExtendedTextField](https://fluttercandies.github.io/extended_text_field/)
- [extended_text_field](#extended_text_field)
- [Limitation](#limitation)
@@ -16,6 +18,9 @@ Language: [English](README.md) | [中文简体](README-ZH.md)
- [Cache Image](#cache-image)
- [TextSelectionControls](#textselectioncontrols)
- [WidgetSpan](#widgetspan)
+ - [NoSystemKeyboard](#nosystemkeyboard)
+ - [TextInputBindingMixin](#textinputbindingmixin)
+ - [TextInputFocusNode](#textinputfocusnode)
## Limitation
@@ -502,5 +507,135 @@ class EmailText extends SpecialText {
}
```
+## NoSystemKeyboard
+
+support to prevent system keyboard show without any code intrusion for [ExtendedTextField] or [TextField].
+
+### TextInputBindingMixin
+
+we prevent system keyboard show by stop Flutter Framework send `TextInput.show` message to Flutter Engine.
+
+you can use [TextInputBinding] directly.
+
+``` dart
+void main() {
+ TextInputBinding();
+ runApp(const MyApp());
+}
+```
+
+or if you have other `binding` you can do as following.
+
+``` dart
+ class YourBinding extends WidgetsFlutterBinding with TextInputBindingMixin,YourBindingMixin {
+ }
+
+ void main() {
+ YourBinding();
+ runApp(const MyApp());
+ }
+```
+
+or you need to override `ignoreTextInputShow`, you can do as following.
+
+``` dart
+ class YourBinding extends TextInputBinding {
+ @override
+ // ignore: unnecessary_overrides
+ bool ignoreTextInputShow() {
+ // you can override it base on your case
+ // if NoKeyboardFocusNode is not enough
+ return super.ignoreTextInputShow();
+ }
+ }
+
+ void main() {
+ YourBinding();
+ runApp(const MyApp());
+ }
+```
+
+### TextInputFocusNode
+
+you should pass the [TextInputFocusNode] into [ExtendedTextField] or [TextField].
+
+``` dart
+final TextInputFocusNode _focusNode = TextInputFocusNode();
+
+ @override
+ Widget build(BuildContext context) {
+ return ExtendedTextField(
+ // request keyboard if need
+ focusNode: _focusNode..debugLabel = 'ExtendedTextField',
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ // request keyboard if need
+ focusNode: _focusNode..debugLabel = 'CustomTextField',
+ );
+ }
+```
+
+we prevent system keyboard show base on current focus is [TextInputFocusNode] and `ignoreSystemKeyboardShow` is true。
+
+``` dart
+ final FocusNode? focus = FocusManager.instance.primaryFocus;
+ if (focus != null &&
+ focus is TextInputFocusNode &&
+ focus.ignoreSystemKeyboardShow) {
+ return true;
+ }
+
+### CustomKeyboard
+
+show/hide your custom keyboard on [TextInputFocusNode] focus is changed.
+
+if your custom keyboard can be close without unFocus, you need also handle
+show custom keyboard when [ExtendedTextField] or [TextField] `onTap`.
+
+``` dart
+ @override
+ void initState() {
+ super.initState();
+ _focusNode.addListener(_handleFocusChanged);
+ }
+
+ void _onTextFiledTap() {
+ if (_bottomSheetController == null) {
+ _handleFocusChanged();
+ }
+ }
+
+ void _handleFocusChanged() {
+ if (_focusNode.hasFocus) {
+ // just demo, you can define your custom keyboard as you want
+ _bottomSheetController = showBottomSheet(
+ context: FocusManager.instance.primaryFocus!.context!,
+ // set false, if don't want to drag to close custom keyboard
+ enableDrag: true,
+ builder: (BuildContext b) {
+ // your custom keyboard
+ return Container();
+ });
+ // maybe drag close
+ _bottomSheetController?.closed.whenComplete(() {
+ _bottomSheetController = null;
+ });
+ } else {
+ _bottomSheetController?.close();
+ _bottomSheetController = null;
+ }
+ }
+
+ @override
+ void dispose() {
+ _focusNode.removeListener(_handleFocusChanged);
+ super.dispose();
+ }
+```
+see [Full Demo](https://github.com/fluttercandies/extended_text_field/tree/master/example/lib/pages/simple/no_keyboard.dart)
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 4b0da0b..a995197 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -173,7 +173,7 @@ linter:
- slash_for_doc_comments
# - sort_child_properties_last # not yet tested
- sort_constructors_first
- - sort_pub_dependencies
+ # - sort_pub_dependencies
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies
deleted file mode 100644
index 930679c..0000000
--- a/example/.flutter-plugins-dependencies
+++ /dev/null
@@ -1 +0,0 @@
-{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"url_launcher","path":"E:\\\\Flutter\\\\flutter_source\\\\1.22.6\\\\.pub-cache\\\\hosted\\\\pub.flutter-io.cn\\\\url_launcher-5.3.0\\\\","dependencies":[]}],"android":[{"name":"url_launcher","path":"E:\\\\Flutter\\\\flutter_source\\\\1.22.6\\\\.pub-cache\\\\hosted\\\\pub.flutter-io.cn\\\\url_launcher-5.3.0\\\\","dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[{"name":"url_launcher_web","path":"E:\\\\Flutter\\\\flutter_source\\\\1.22.6\\\\.pub-cache\\\\hosted\\\\pub.flutter-io.cn\\\\url_launcher_web-0.1.5+3\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"url_launcher","dependencies":["url_launcher_web"]},{"name":"url_launcher_web","dependencies":[]}],"date_created":"2021-04-24 11:52:26.206055","version":"1.22.6"}
\ No newline at end of file
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 0000000..4656aa0
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,207 @@
+# Specify analysis options.
+#
+# Until there are meta linter rules, each desired lint must be explicitly enabled.
+# See: https://github.com/dart-lang/linter/issues/288
+#
+# For a list of lints, see: http://dart-lang.github.io/linter/lints/
+# See the configuration guide for more
+# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
+#
+# There are other similar analysis options files in the flutter repos,
+# which should be kept in sync with this file:
+#
+# - analysis_options.yaml (this file)
+# - packages/flutter/lib/analysis_options_user.yaml
+# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
+# - https://github.com/flutter/engine/blob/master/analysis_options.yaml
+#
+# This file contains the analysis options used by Flutter tools, such as IntelliJ,
+# Android Studio, and the `flutter analyze` command.
+
+analyzer:
+ strong-mode:
+ implicit-casts: false
+ implicit-dynamic: false
+ errors:
+ # treat missing required parameters as a warning (not a hint)
+ missing_required_param: warning
+ # treat missing returns as a warning (not a hint)
+ missing_return: warning
+ # allow having TODOs in the code
+ todo: ignore
+ # Ignore analyzer hints for updating pubspecs when using Future or
+ # Stream and not importing dart:async
+ # Please see https://github.com/flutter/flutter/pull/24528 for details.
+ sdk_version_async_exported_from_core: ignore
+ # exclude:
+ # - "bin/cache/**"
+ # # the following two are relative to the stocks example and the flutter package respectively
+ # # see https://github.com/dart-lang/sdk/issues/28463
+ # - "lib/i18n/messages_*.dart"
+ # - "lib/src/http/**"
+
+linter:
+ rules:
+ # these rules are documented on and in the same order as
+ # the Dart Lint rules page to make maintenance easier
+ # https://github.com/dart-lang/linter/blob/master/example/all.yaml
+ - always_declare_return_types
+ - always_put_control_body_on_new_line
+ # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219
+ - always_require_non_null_named_parameters
+ - always_specify_types
+ - annotate_overrides
+ # - avoid_annotating_with_dynamic # conflicts with always_specify_types
+ # - avoid_as # required for implicit-casts: true
+ - avoid_bool_literals_in_conditional_expressions
+ # - avoid_catches_without_on_clauses # we do this commonly
+ # - avoid_catching_errors # we do this commonly
+ - avoid_classes_with_only_static_members
+ # - avoid_double_and_int_checks # only useful when targeting JS runtime
+ - avoid_empty_else
+ # - avoid_equals_and_hash_code_on_mutable_classes # not yet tested
+ - avoid_field_initializers_in_const_classes
+ - avoid_function_literals_in_foreach_calls
+ # - avoid_implementing_value_types # not yet tested
+ - avoid_init_to_null
+ # - avoid_js_rounded_ints # only useful when targeting JS runtime
+ - avoid_null_checks_in_equality_operators
+ # - avoid_positional_boolean_parameters # not yet tested
+ # - avoid_print # not yet tested
+ # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356)
+ # - avoid_redundant_argument_values # not yet tested
+ - avoid_relative_lib_imports
+ - avoid_renaming_method_parameters
+ - avoid_return_types_on_setters
+ # - avoid_returning_null # there are plenty of valid reasons to return null
+ # - avoid_returning_null_for_future # not yet tested
+ - avoid_returning_null_for_void
+ # - avoid_returning_this # there are plenty of valid reasons to return this
+ # - avoid_setters_without_getters # not yet tested
+ # - avoid_shadowing_type_parameters # not yet tested
+ - avoid_single_cascade_in_expression_statements
+ - avoid_slow_async_io
+ - avoid_types_as_parameter_names
+ # - avoid_types_on_closure_parameters # conflicts with always_specify_types
+ # - avoid_unnecessary_containers # not yet tested
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ # - avoid_web_libraries_in_flutter # not yet tested
+ - await_only_futures
+ - camel_case_extensions
+ - camel_case_types
+ - cancel_subscriptions
+ # - cascade_invocations # not yet tested
+ # - close_sinks # not reliable enough
+ # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765
+ # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204
+ - control_flow_in_finally
+ # - curly_braces_in_flow_control_structures # not yet tested
+ # - diagnostic_describe_all_properties # not yet tested
+ - directives_ordering
+ - empty_catches
+ - empty_constructor_bodies
+ - empty_statements
+ # - file_names # not yet tested
+ - flutter_style_todos
+ - hash_and_equals
+ - implementation_imports
+ # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811
+ - iterable_contains_unrelated_type
+ # - join_return_with_assignment # not yet tested
+ - library_names
+ - library_prefixes
+ # - lines_longer_than_80_chars # not yet tested
+ - list_remove_unrelated_type
+ # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181
+ # - missing_whitespace_between_adjacent_strings # not yet tested
+ - no_adjacent_strings_in_list
+ - no_duplicate_case_values
+ # - no_logic_in_create_state # not yet tested
+ # - no_runtimeType_toString # not yet tested
+ - non_constant_identifier_names
+ # - null_closures # not yet tested
+ # - omit_local_variable_types # opposite of always_specify_types
+ # - one_member_abstracts # too many false positives
+ # - only_throw_errors # https://github.com/flutter/flutter/issues/5792
+ - overridden_fields
+ - package_api_docs
+ - package_names
+ - package_prefixed_library_names
+ # - parameter_assignments # we do this commonly
+ - prefer_adjacent_string_concatenation
+ - prefer_asserts_in_initializer_lists
+ # - prefer_asserts_with_message # not yet tested
+ - prefer_collection_literals
+ - prefer_conditional_assignment
+ - prefer_const_constructors
+ - prefer_const_constructors_in_immutables
+ - prefer_const_declarations
+ - prefer_const_literals_to_create_immutables
+ # - prefer_constructors_over_static_methods # not yet tested
+ - prefer_contains
+ # - prefer_double_quotes # opposite of prefer_single_quotes
+ - prefer_equal_for_default_values
+ # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods
+ - prefer_final_fields
+ - prefer_final_in_for_each
+ - prefer_final_locals
+ - prefer_for_elements_to_map_fromIterable
+ - prefer_foreach
+ # - prefer_function_declarations_over_variables # not yet tested
+ - prefer_generic_function_type_aliases
+ - prefer_if_elements_to_conditional_expressions
+ - prefer_if_null_operators
+ - prefer_initializing_formals
+ - prefer_inlined_adds
+ # - prefer_int_literals # not yet tested
+ # - prefer_interpolation_to_compose_strings # not yet tested
+ - prefer_is_empty
+ - prefer_is_not_empty
+ - prefer_is_not_operator
+ - prefer_iterable_whereType
+ # - prefer_mixin # https://github.com/dart-lang/language/issues/32
+ # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932
+ # - prefer_relative_imports # not yet tested
+ - prefer_single_quotes
+ - prefer_spread_collections
+ - prefer_typing_uninitialized_variables
+ - prefer_void_to_null
+ # - provide_deprecation_message # not yet tested
+ # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml
+ - recursive_getters
+ - slash_for_doc_comments
+ #- sort_child_properties_last # not yet tested
+ - sort_constructors_first
+ #- sort_pub_dependencies
+ - sort_unnamed_constructors_first
+ - test_types_in_equals
+ - throw_in_finally
+ # - type_annotate_public_apis # subset of always_specify_types
+ - type_init_formals
+ # - unawaited_futures # too many false positives
+ # - unnecessary_await_in_return # not yet tested
+ - unnecessary_brace_in_string_interps
+ - unnecessary_const
+ # - unnecessary_final # conflicts with prefer_final_locals
+ - unnecessary_getters_setters
+ # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498
+ - unnecessary_new
+ - unnecessary_null_aware_assignments
+ - unnecessary_null_in_if_null_operators
+ - unnecessary_overrides
+ - unnecessary_parenthesis
+ - unnecessary_statements
+ - unnecessary_string_interpolations
+ - unnecessary_this
+ - unrelated_type_equality_checks
+ # - unsafe_html # not yet tested
+ - use_full_hex_values_for_flutter_colors
+ # - use_function_type_syntax_for_parameters # not yet tested
+ # - use_key_in_widget_constructors # not yet tested
+ - use_rethrow_when_possible
+ # - use_setters_to_change_properties # not yet tested
+ # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182
+ # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review
+ - valid_regexps
+ - void_checks
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 3932aa9..5fe3c92 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -26,21 +26,26 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 29
+ compileSdkVersion flutter.compileSdkVersion
- sourceSets {
- main.java.srcDirs += 'src/main/kotlin'
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
}
- lintOptions {
- disable 'InvalidPackage'
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
- minSdkVersion 16
- targetSdkVersion 29
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index 55ca830..3f41384 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -1,16 +1,12 @@
-
-
-
-
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
index 449a9f9..3db14bb 100644
--- a/example/android/app/src/main/res/values-night/styles.xml
+++ b/example/android/app/src/main/res/values-night/styles.xml
@@ -10,7 +10,7 @@
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
-
+
This Theme is only used starting with V2 of Flutter's Android embedding. -->
diff --git a/example/android/build.gradle b/example/android/build.gradle
index 3100ad2..4256f91 100644
--- a/example/android/build.gradle
+++ b/example/android/build.gradle
@@ -1,12 +1,12 @@
buildscript {
- ext.kotlin_version = '1.3.50'
+ ext.kotlin_version = '1.6.10'
repositories {
google()
- jcenter()
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath 'com.android.tools.build:gradle:4.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@@ -14,7 +14,7 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
index a673820..94adc3a 100644
--- a/example/android/gradle.properties
+++ b/example/android/gradle.properties
@@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
-android.enableR8=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
index 296b146..bc6a58a 100644
--- a/example/android/gradle/wrapper/gradle-wrapper.properties
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/example/ff_annotation_route_commands b/example/ff_annotation_route_commands
index e1ed45b..ebf8ab2 100644
--- a/example/ff_annotation_route_commands
+++ b/example/ff_annotation_route_commands
@@ -1 +1 @@
---route-constants --route-names --route-helper --git flutter_candies_demo_library --no-is-initial-route
\ No newline at end of file
+-s
\ No newline at end of file
diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id
deleted file mode 100644
index d1054b8..0000000
--- a/example/ios/Flutter/.last_build_id
+++ /dev/null
@@ -1 +0,0 @@
-afab667b626c6581508496d02f18db69
\ No newline at end of file
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..4f8d4d2 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 11.0
diff --git a/example/ios/Podfile b/example/ios/Podfile
index 1e8c3c9..88359b2 100644
--- a/example/ios/Podfile
+++ b/example/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-# platform :ios, '9.0'
+# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 0cace26..377a985 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -1,22 +1,22 @@
PODS:
- Flutter (1.0.0)
- - url_launcher (0.0.1):
+ - url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- - url_launcher (from `.symlinks/plugins/url_launcher/ios`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
- url_launcher:
- :path: ".symlinks/plugins/url_launcher/ios"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
- Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
- url_launcher: a1c0cc845906122c4784c542523d8cacbded5626
+ Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+ url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
-PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
-COCOAPODS: 1.10.0
+COCOAPODS: 1.11.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index eee1362..9184c9e 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1020;
+ LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -222,6 +222,7 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -236,6 +237,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -340,7 +342,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -422,7 +424,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -471,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
index 1d526a1..919434a 100644
--- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -2,6 +2,6 @@
+ location = "self:">
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index a28140c..3db53b6 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
UIViewControllerBasedStatusBarAppearance
+ CADisableMinimumFrameDurationOnPhone
+
+ UIApplicationSupportsIndirectInputEvents
+
diff --git a/example/lib/common/toggle_button.dart b/example/lib/common/toggle_button.dart
index 6fe5e9c..f96fd63 100644
--- a/example/lib/common/toggle_button.dart
+++ b/example/lib/common/toggle_button.dart
@@ -6,10 +6,10 @@ class ToggleButton extends StatefulWidget {
this.unActiveWidget,
this.activeChanged,
this.active = false});
- final Widget activeWidget;
- final Widget unActiveWidget;
+ final Widget? activeWidget;
+ final Widget? unActiveWidget;
final bool active;
- final ValueChanged activeChanged;
+ final ValueChanged? activeChanged;
@override
_ToggleButtonState createState() => _ToggleButtonState();
}
diff --git a/example/lib/example_route.dart b/example/lib/example_route.dart
index 67735fa..f95adee 100644
--- a/example/lib/example_route.dart
+++ b/example/lib/example_route.dart
@@ -2,92 +2,103 @@
// **************************************************************************
// Auto generated by https://github.com/fluttercandies/ff_annotation_route
// **************************************************************************
-
-import 'package:ff_annotation_route/ff_annotation_route.dart';
+// fast mode: true
+// version: 10.0.6
+// **************************************************************************
+// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import,unused_shown_name,implementation_imports,duplicate_import
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/widgets.dart';
+
import 'pages/complex/text_demo.dart';
import 'pages/main_page.dart';
import 'pages/simple/custom_toolbar.dart';
+import 'pages/simple/no_keyboard.dart';
import 'pages/simple/widget_span.dart';
-RouteResult getRouteResult({String name, Map arguments}) {
- arguments = arguments ?? const {};
+FFRouteSettings getRouteSettings({
+ required String name,
+ Map? arguments,
+ PageBuilder? notFoundPageBuilder,
+}) {
+ final Map safeArguments =
+ arguments ?? const {};
switch (name) {
case 'fluttercandies://CustomToolBar':
- return RouteResult(
+ return FFRouteSettings(
name: name,
- widget: CustomToolBar(),
+ arguments: arguments,
+ builder: () => CustomToolBar(),
routeName: 'custom toolbar',
description: 'custom selection toolbar and handles for text field',
- exts: {'group': 'Simple', 'order': 0},
+ exts: {
+ 'group': 'Simple',
+ 'order': 0,
+ },
+ );
+ case 'fluttercandies://NoKeyboard':
+ return FFRouteSettings(
+ name: name,
+ arguments: arguments,
+ builder: () => NoSystemKeyboardDemo(
+ key: asT(
+ safeArguments['key'],
+ ),
+ ),
+ routeName: 'no system Keyboard',
+ description:
+ 'show how to ignore system keyboard and show custom keyboard',
+ exts: {
+ 'group': 'Simple',
+ 'order': 2,
+ },
);
case 'fluttercandies://TextDemo':
- return RouteResult(
+ return FFRouteSettings(
name: name,
- widget: TextDemo(),
+ arguments: arguments,
+ builder: () => TextDemo(),
routeName: 'text',
description: 'build special text and inline image in text field',
- exts: {'group': 'Complex', 'order': 0},
+ exts: {
+ 'group': 'Complex',
+ 'order': 0,
+ },
);
case 'fluttercandies://WidgetSpanDemo':
- return RouteResult(
+ return FFRouteSettings(
name: name,
- widget: WidgetSpanDemo(),
+ arguments: arguments,
+ builder: () => WidgetSpanDemo(),
routeName: 'widget span',
description: 'mailbox demo with widgetSpan',
- exts: {'group': 'Simple', 'order': 1},
+ exts: {
+ 'group': 'Simple',
+ 'order': 1,
+ },
);
case 'fluttercandies://demogrouppage':
- return RouteResult(
+ return FFRouteSettings(
name: name,
- widget: DemoGroupPage(
- keyValue:
- arguments['keyValue'] as MapEntry>,
+ arguments: arguments,
+ builder: () => DemoGroupPage(
+ keyValue: asT>>(
+ safeArguments['keyValue'],
+ )!,
),
routeName: 'DemoGroupPage',
);
case 'fluttercandies://mainpage':
- return RouteResult(
+ return FFRouteSettings(
name: name,
- widget: MainPage(),
+ arguments: arguments,
+ builder: () => MainPage(),
routeName: 'MainPage',
);
default:
- return const RouteResult(name: 'flutterCandies://notfound');
+ return FFRouteSettings(
+ name: FFRoute.notFoundName,
+ routeName: FFRoute.notFoundRouteName,
+ builder: notFoundPageBuilder ?? () => Container(),
+ );
}
}
-
-class RouteResult {
- const RouteResult({
- @required this.name,
- this.widget,
- this.showStatusBar = true,
- this.routeName = '',
- this.pageRouteType,
- this.description = '',
- this.exts,
- });
-
- /// The name of the route (e.g., "/settings").
- ///
- /// If null, the route is anonymous.
- final String name;
-
- /// The Widget return base on route
- final Widget widget;
-
- /// Whether show this route with status bar.
- final bool showStatusBar;
-
- /// The route name to track page
- final String routeName;
-
- /// The type of page route
- final PageRouteType pageRouteType;
-
- /// The description of route
- final String description;
-
- /// The extend arguments
- final Map exts;
-}
diff --git a/example/lib/example_route_helper.dart b/example/lib/example_route_helper.dart
deleted file mode 100644
index 2ef518e..0000000
--- a/example/lib/example_route_helper.dart
+++ /dev/null
@@ -1,188 +0,0 @@
-// GENERATED CODE - DO NOT MODIFY MANUALLY
-// **************************************************************************
-// Auto generated by https://github.com/fluttercandies/ff_annotation_route
-// **************************************************************************
-
-import 'dart:io';
-
-import 'package:flutter/material.dart';
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/widgets.dart';
-import 'package:ff_annotation_route/ff_annotation_route.dart';
-
-import 'example_route.dart';
-
-class FFNavigatorObserver extends NavigatorObserver {
- FFNavigatorObserver({this.routeChange});
-
- final RouteChange routeChange;
-
- @override
- void didPop(Route route, Route previousRoute) {
- super.didPop(route, previousRoute);
- _didRouteChange(previousRoute, route);
- }
-
- @override
- void didPush(Route route, Route previousRoute) {
- super.didPush(route, previousRoute);
- _didRouteChange(route, previousRoute);
- }
-
- @override
- void didRemove(Route route, Route previousRoute) {
- super.didRemove(route, previousRoute);
- _didRouteChange(previousRoute, route);
- }
-
- @override
- void didReplace({Route newRoute, Route oldRoute}) {
- super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
- _didRouteChange(newRoute, oldRoute);
- }
-
- void _didRouteChange(Route newRoute, Route oldRoute) {
- // oldRoute may be null when route first time enter.
- routeChange?.call(newRoute, oldRoute);
- }
-}
-
-typedef RouteChange = void Function(
- Route newRoute, Route oldRoute);
-
-class FFTransparentPageRoute extends PageRouteBuilder {
- FFTransparentPageRoute({
- RouteSettings settings,
- @required RoutePageBuilder pageBuilder,
- RouteTransitionsBuilder transitionsBuilder = _defaultTransitionsBuilder,
- Duration transitionDuration = const Duration(milliseconds: 150),
- bool barrierDismissible = false,
- Color barrierColor,
- String barrierLabel,
- bool maintainState = true,
- }) : assert(pageBuilder != null),
- assert(transitionsBuilder != null),
- assert(barrierDismissible != null),
- assert(maintainState != null),
- super(
- settings: settings,
- opaque: false,
- pageBuilder: pageBuilder,
- transitionsBuilder: transitionsBuilder,
- transitionDuration: transitionDuration,
- barrierDismissible: barrierDismissible,
- barrierColor: barrierColor,
- barrierLabel: barrierLabel,
- maintainState: maintainState,
- );
-}
-
-Widget _defaultTransitionsBuilder(
- BuildContext context,
- Animation animation,
- Animation secondaryAnimation,
- Widget child,
-) {
- return FadeTransition(
- opacity: CurvedAnimation(
- parent: animation,
- curve: Curves.easeOut,
- ),
- child: child,
- );
-}
-
-Route onGenerateRouteHelper(
- RouteSettings settings, {
- Widget notFoundFallback,
- Object arguments,
- WidgetBuilder builder,
-}) {
- arguments ??= settings.arguments;
-
- final RouteResult routeResult = getRouteResult(
- name: settings.name,
- arguments: arguments as Map,
- );
- if (routeResult.showStatusBar != null || routeResult.routeName != null) {
- settings = FFRouteSettings(
- name: settings.name,
- routeName: routeResult.routeName,
- arguments: arguments as Map,
- showStatusBar: routeResult.showStatusBar,
- );
- }
- Widget page = routeResult.widget ?? notFoundFallback;
- if (page == null) {
- throw Exception(
- '''Route "${settings.name}" returned null. Route Widget must never return null,
- maybe the reason is that route name did not match with right path.
- You can use parameter[notFoundFallback] to avoid this ugly error.''',
- );
- }
-
- if (arguments is Map) {
- final RouteBuilder builder = arguments['routeBuilder'] as RouteBuilder;
- if (builder != null) {
- return builder(page);
- }
- }
-
- if (builder != null) {
- page = builder(page, routeResult);
- }
-
- switch (routeResult.pageRouteType) {
- case PageRouteType.material:
- return MaterialPageRoute(
- settings: settings,
- builder: (BuildContext _) => page,
- );
- case PageRouteType.cupertino:
- return CupertinoPageRoute(
- settings: settings,
- builder: (BuildContext _) => page,
- );
- case PageRouteType.transparent:
- return FFTransparentPageRoute(
- settings: settings,
- pageBuilder: (
- BuildContext _,
- Animation __,
- Animation ___,
- ) =>
- page,
- );
- default:
- return kIsWeb || !Platform.isIOS
- ? MaterialPageRoute(
- settings: settings,
- builder: (BuildContext _) => page,
- )
- : CupertinoPageRoute(
- settings: settings,
- builder: (BuildContext _) => page,
- );
- }
-}
-
-typedef RouteBuilder = PageRoute Function(Widget page);
-
-class FFRouteSettings extends RouteSettings {
- const FFRouteSettings({
- this.routeName,
- this.showStatusBar,
- String name,
- Object arguments,
- }) : super(
- name: name,
- arguments: arguments,
- );
-
- final String routeName;
- final bool showStatusBar;
-}
-
-/// Signature for a function that creates a widget, e.g.
-typedef WidgetBuilder = Widget Function(Widget child, RouteResult routeResult);
diff --git a/example/lib/example_routes.dart b/example/lib/example_routes.dart
index 64a30b9..ac72491 100644
--- a/example/lib/example_routes.dart
+++ b/example/lib/example_routes.dart
@@ -2,8 +2,13 @@
// **************************************************************************
// Auto generated by https://github.com/fluttercandies/ff_annotation_route
// **************************************************************************
+// fast mode: true
+// version: 10.0.6
+// **************************************************************************
+// ignore_for_file: prefer_const_literals_to_create_immutables,unused_local_variable,unused_import,unnecessary_import,unused_shown_name,implementation_imports,duplicate_import
const List routeNames = [
'fluttercandies://CustomToolBar',
+ 'fluttercandies://NoKeyboard',
'fluttercandies://TextDemo',
'fluttercandies://WidgetSpanDemo',
'fluttercandies://demogrouppage',
@@ -21,10 +26,25 @@ class Routes {
///
/// [description] : 'custom selection toolbar and handles for text field'
///
- /// [exts] : {group: Simple, order: 0}
+ /// [exts] : {'group': 'Simple', 'order': 0}
static const String fluttercandiesCustomToolBar =
'fluttercandies://CustomToolBar';
+ /// 'show how to ignore system keyboard and show custom keyboard'
+ ///
+ /// [name] : 'fluttercandies://NoKeyboard'
+ ///
+ /// [routeName] : 'no system Keyboard'
+ ///
+ /// [description] : 'show how to ignore system keyboard and show custom keyboard'
+ ///
+ /// [constructors] :
+ ///
+ /// NoSystemKeyboardDemo : [Key? key]
+ ///
+ /// [exts] : {'group': 'Simple', 'order': 2}
+ static const String fluttercandiesNoKeyboard = 'fluttercandies://NoKeyboard';
+
/// 'build special text and inline image in text field'
///
/// [name] : 'fluttercandies://TextDemo'
@@ -33,7 +53,7 @@ class Routes {
///
/// [description] : 'build special text and inline image in text field'
///
- /// [exts] : {group: Complex, order: 0}
+ /// [exts] : {'group': 'Complex', 'order': 0}
static const String fluttercandiesTextDemo = 'fluttercandies://TextDemo';
/// 'mailbox demo with widgetSpan'
@@ -44,7 +64,7 @@ class Routes {
///
/// [description] : 'mailbox demo with widgetSpan'
///
- /// [exts] : {group: Simple, order: 1}
+ /// [exts] : {'group': 'Simple', 'order': 1}
static const String fluttercandiesWidgetSpanDemo =
'fluttercandies://WidgetSpanDemo';
@@ -56,7 +76,7 @@ class Routes {
///
/// [constructors] :
///
- /// DemoGroupPage : [MapEntry> keyValue]
+ /// DemoGroupPage : [MapEntry>(required) keyValue]
static const String fluttercandiesDemogrouppage =
'fluttercandies://demogrouppage';
diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart
index 2973833..40a0d00 100644
--- a/example/lib/generated_plugin_registrant.dart
+++ b/example/lib/generated_plugin_registrant.dart
@@ -2,15 +2,16 @@
// Generated file. Do not edit.
//
-// ignore: unused_import
-import 'dart:ui';
+// ignore_for_file: directives_ordering
+// ignore_for_file: lines_longer_than_80_chars
+// ignore_for_file: depend_on_referenced_packages
import 'package:url_launcher_web/url_launcher_web.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
// ignore: public_member_api_docs
-void registerPlugins(PluginRegistry registry) {
- UrlLauncherPlugin.registerWith(registry.registrarFor(UrlLauncherPlugin));
- registry.registerMessageHandler();
+void registerPlugins(Registrar registrar) {
+ UrlLauncherPlugin.registerWith(registrar);
+ registrar.registerMessageHandler();
}
diff --git a/example/lib/main.dart b/example/lib/main.dart
index e790a6b..4b943b0 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,70 +1,36 @@
-import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
+import 'package:example/pages/simple/no_keyboard.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/material.dart';
+import 'package:oktoast/oktoast.dart';
import 'example_route.dart';
-import 'example_route_helper.dart';
import 'example_routes.dart';
-void main() => runApp(MyApp());
+void main() {
+ CustomKeyboarBinding();
+ runApp(MyApp());
+}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
- return MaterialApp(
- title: 'extended_text_field demo',
- debugShowCheckedModeBanner: false,
- theme: ThemeData(
- primarySwatch: Colors.blue,
- ),
- initialRoute: Routes.fluttercandiesMainpage,
- onGenerateRoute: (RouteSettings settings) {
- //when refresh web, route will as following
- // /
- // /fluttercandies:
- // /fluttercandies:/
- // /fluttercandies://mainpage
- if (kIsWeb && settings.name.startsWith('/')) {
- return onGenerateRouteHelper(
- settings.copyWith(name: settings.name.replaceFirst('/', '')),
- notFoundFallback:
- getRouteResult(name: Routes.fluttercandiesMainpage).widget,
- );
- }
- return onGenerateRouteHelper(settings,
- builder: (Widget child, RouteResult result) {
- return child;
- // if (settings.name == Routes.fluttercandiesMainpage ||
- // settings.name == Routes.fluttercandiesDemogrouppage) {
- // return child;
- // }
- // return CommonWidget(
- // child: child,
- // result: result,
- // );
- });
- },
- );
- }
-}
-
-class CommonWidget extends StatelessWidget {
- const CommonWidget({
- this.child,
- this.result,
- });
- final Widget child;
- final RouteResult result;
-
- @override
- Widget build(BuildContext context) {
- return Scaffold(
- appBar: AppBar(
- title: Text(
- result.routeName,
+ //EditableText
+ //TextField
+ return OKToast(
+ child: MaterialApp(
+ title: 'extended_text_field demo',
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
),
+ initialRoute: Routes.fluttercandiesMainpage,
+ onGenerateRoute: (RouteSettings settings) {
+ return onGenerateRoute(
+ settings: settings,
+ getRouteSettings: getRouteSettings,
+ );
+ },
),
- body: child,
);
}
}
diff --git a/example/lib/pages/complex/text_demo.dart b/example/lib/pages/complex/text_demo.dart
index c4f8c22..fee79e1 100644
--- a/example/lib/pages/complex/text_demo.dart
+++ b/example/lib/pages/complex/text_demo.dart
@@ -1,17 +1,20 @@
+import 'dart:async';
+import 'dart:io';
import 'dart:math';
+
import 'package:example/common/toggle_button.dart';
import 'package:example/special_text/at_text.dart';
import 'package:example/special_text/dollar_text.dart';
+import 'package:example/special_text/emoji_text.dart' as emoji;
import 'package:example/special_text/my_extended_text_selection_controls.dart';
import 'package:example/special_text/my_special_text_span_builder.dart';
import 'package:extended_list/extended_list.dart';
import 'package:extended_text/extended_text.dart';
-import 'package:flutter/material.dart';
import 'package:extended_text_field/extended_text_field.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
-import 'package:ff_annotation_route/ff_annotation_route.dart';
-import 'package:loading_more_list/loading_more_list.dart';
-import 'package:example/special_text/emoji_text.dart' as emoji;
import 'package:url_launcher/url_launcher.dart';
@FFRoute(
@@ -30,15 +33,18 @@ class TextDemo extends StatefulWidget {
class _TextDemoState extends State {
final TextEditingController _textEditingController = TextEditingController();
- final MyExtendedMaterialTextSelectionControls
- _myExtendedMaterialTextSelectionControls =
- MyExtendedMaterialTextSelectionControls();
- final GlobalKey _key = GlobalKey();
+ final MyTextSelectionControls _myExtendedMaterialTextSelectionControls =
+ MyTextSelectionControls();
+ final GlobalKey _key =
+ GlobalKey();
final MySpecialTextSpanBuilder _mySpecialTextSpanBuilder =
MySpecialTextSpanBuilder();
+ final StreamController _gridBuilderController =
+ StreamController.broadcast();
final FocusNode _focusNode = FocusNode();
- double _keyboardHeight = 267.0;
+ double _keyboardHeight = 0;
+ double _preKeyboardHeight = 0;
bool get showCustomKeyBoard =>
activeEmojiGird || activeAtGrid || activeDollarGrid;
bool activeEmojiGird = false;
@@ -55,215 +61,247 @@ class _TextDemoState extends State {
@override
Widget build(BuildContext context) {
- FocusScope.of(context).autofocus(_focusNode);
+ //FocusScope.of(context).autofocus(_focusNode);
+ final MediaQueryData mediaQueryData = MediaQuery.of(context);
final double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
- if (keyboardHeight > 0) {
+
+ final bool showingKeyboard = keyboardHeight > _preKeyboardHeight;
+ _preKeyboardHeight = keyboardHeight;
+ if ((keyboardHeight > 0 && keyboardHeight >= _keyboardHeight) ||
+ showingKeyboard) {
activeEmojiGird = activeAtGrid = activeDollarGrid = false;
+ _gridBuilderController.add(null);
}
_keyboardHeight = max(_keyboardHeight, keyboardHeight);
- return Scaffold(
- appBar: AppBar(
- title: const Text('special text'),
- actions: [
- FlatButton(
- child: const Icon(Icons.backspace),
- onPressed: manualDelete,
- )
- ],
- ),
- body: Column(
- children: [
- Expanded(
- child: ExtendedListView.builder(
- extendedListDelegate:
- const ExtendedListDelegate(closeToTrailing: true),
- itemBuilder: (BuildContext context, int index) {
- final bool left = index % 2 == 0;
- final Image logo = Image.asset(
- 'assets/flutter_candies_logo.png',
- width: 30.0,
- height: 30.0,
- );
- //print(sessions[index]);
- final Widget text = ExtendedText(
- sessions[index],
- textAlign: left ? TextAlign.left : TextAlign.right,
- specialTextSpanBuilder: _mySpecialTextSpanBuilder,
- onSpecialTextTap: (dynamic value) {
- if (value.toString().startsWith('\$')) {
- launch('https://github.com/fluttercandies');
- } else if (value.toString().startsWith('@')) {
- launch('mailto:zmtzawqlp@live.com');
- }
- },
- );
- List list = [
- logo,
- Expanded(child: text),
- Container(
+ return SafeArea(
+ bottom: true,
+ child: Scaffold(
+ resizeToAvoidBottomInset: false,
+ appBar: AppBar(
+ title: const Text('special text'),
+ actions: [
+ TextButton(
+ child: const Icon(
+ Icons.backspace,
+ color: Colors.white,
+ ),
+ onPressed: manualDelete,
+ )
+ ],
+ ),
+ body: Column(
+ children: [
+ Expanded(
+ child: ExtendedListView.builder(
+ extendedListDelegate:
+ const ExtendedListDelegate(closeToTrailing: true),
+ itemBuilder: (BuildContext context, int index) {
+ final bool left = index % 2 == 0;
+ final Image logo = Image.asset(
+ 'assets/flutter_candies_logo.png',
width: 30.0,
- )
- ];
- if (!left) {
- list = list.reversed.toList();
- }
- return Row(
- children: list,
- );
- },
- padding: const EdgeInsets.only(bottom: 10.0),
- reverse: true,
- itemCount: sessions.length,
- )),
- // TextField()
- Container(
- height: 2.0,
- color: Colors.blue,
- ),
- ExtendedTextField(
- key: _key,
- specialTextSpanBuilder: MySpecialTextSpanBuilder(
- showAtBackground: true,
- ),
- controller: _textEditingController,
- textSelectionControls: _myExtendedMaterialTextSelectionControls,
- maxLines: null,
- focusNode: _focusNode,
- decoration: InputDecoration(
- suffixIcon: GestureDetector(
- onTap: () {
- setState(() {
- sessions.insert(0, _textEditingController.text);
- _textEditingController.value =
- _textEditingController.value.copyWith(
- text: '',
- selection:
- const TextSelection.collapsed(offset: 0),
- composing: TextRange.empty);
- });
+ height: 30.0,
+ );
+ //print(sessions[index]);
+ final Widget text = ExtendedText(
+ sessions[index],
+ textAlign: left ? TextAlign.left : TextAlign.right,
+ specialTextSpanBuilder: _mySpecialTextSpanBuilder,
+ onSpecialTextTap: (dynamic value) {
+ if (value.toString().startsWith('\$')) {
+ launchUrl(Uri.parse('https://github.com/fluttercandies'));
+ } else if (value.toString().startsWith('@')) {
+ launchUrl(Uri.parse('mailto:zmtzawqlp@live.com'));
+ }
},
- child: const Icon(Icons.send),
- ),
- contentPadding: const EdgeInsets.all(12.0)),
- //textDirection: TextDirection.rtl,
- ),
- Container(
- color: Colors.grey.withOpacity(0.3),
- child: Column(
- children: [
- Row(
- children: [
- ToggleButton(
- activeWidget: const Icon(
- Icons.sentiment_very_satisfied,
- color: Colors.orange,
- ),
- unActiveWidget:
- const Icon(Icons.sentiment_very_satisfied),
- activeChanged: (bool active) {
- final Function change = () {
- setState(() {
- if (active) {
- activeAtGrid = activeDollarGrid = false;
- FocusScope.of(context).requestFocus(_focusNode);
- }
- activeEmojiGird = active;
- });
- };
- update(change);
- },
- active: activeEmojiGird,
- ),
- ToggleButton(
- activeWidget: const Padding(
- padding: EdgeInsets.only(bottom: 5.0),
- child: Text(
- '@',
- style: TextStyle(
+ );
+ List list = [
+ logo,
+ Expanded(child: text),
+ Container(
+ width: 30.0,
+ )
+ ];
+ if (!left) {
+ list = list.reversed.toList();
+ }
+ return Row(
+ children: list,
+ );
+ },
+ padding: const EdgeInsets.only(bottom: 10.0),
+ reverse: true,
+ itemCount: sessions.length,
+ )),
+ // TextField()
+ Container(
+ height: 2.0,
+ color: Colors.blue,
+ ),
+ //EditableText(controller: controller, focusNode: focusNode, style: style, cursorColor: cursorColor, backgroundCursorColor: backgroundCursorColor)
+ ExtendedTextField(
+ key: _key,
+ minLines: 1,
+ maxLines: 2,
+ // StrutStyle get strutStyle {
+ // if (_strutStyle == null) {
+ // return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
+ // }
+ // return _strutStyle!.inheritFromTextStyle(style);
+ // }
+ // default strutStyle is not good for WidgetSpan
+ strutStyle: const StrutStyle(),
+ specialTextSpanBuilder: MySpecialTextSpanBuilder(
+ showAtBackground: true,
+ ),
+ controller: _textEditingController,
+ selectionControls: _myExtendedMaterialTextSelectionControls,
+
+ focusNode: _focusNode,
+ decoration: InputDecoration(
+ suffixIcon: GestureDetector(
+ onTap: () {
+ setState(() {
+ sessions.insert(0, _textEditingController.text);
+ _textEditingController.value =
+ _textEditingController.value.copyWith(
+ text: '',
+ selection:
+ const TextSelection.collapsed(offset: 0),
+ composing: TextRange.empty);
+ });
+ },
+ child: const Icon(Icons.send),
+ ),
+ contentPadding: const EdgeInsets.all(12.0)),
+ //textDirection: TextDirection.rtl,
+ ),
+ StreamBuilder(
+ stream: _gridBuilderController.stream,
+ builder: (BuildContext b, AsyncSnapshot d) {
+ return Container(
+ color: Colors.grey.withOpacity(0.3),
+ child: Column(
+ children: [
+ Row(
+ children: [
+ ToggleButton(
+ activeWidget: const Icon(
+ Icons.sentiment_very_satisfied,
color: Colors.orange,
- fontWeight: FontWeight.bold,
- fontSize: 20.0,
),
+ unActiveWidget:
+ const Icon(Icons.sentiment_very_satisfied),
+ activeChanged: (bool active) {
+ onToolbarButtonActiveChanged(
+ keyboardHeight, active, () {
+ activeEmojiGird = active;
+ });
+ },
+ active: activeEmojiGird,
),
- ),
- unActiveWidget: const Padding(
- padding: EdgeInsets.only(bottom: 5.0),
- child: Text(
- '@',
- style: TextStyle(
- fontWeight: FontWeight.bold, fontSize: 20.0),
- ),
- ),
- activeChanged: (bool active) {
- final Function change = () {
- setState(() {
- if (active) {
- activeEmojiGird = activeDollarGrid = false;
- FocusScope.of(context).requestFocus(_focusNode);
- }
- activeAtGrid = active;
- });
- };
- update(change);
- },
- active: activeAtGrid),
- ToggleButton(
- activeWidget: const Icon(
- Icons.attach_money,
- color: Colors.orange,
- ),
- unActiveWidget: const Icon(Icons.attach_money),
- activeChanged: (bool active) {
- final Function change = () {
- setState(() {
- if (active) {
- activeEmojiGird = activeAtGrid = false;
- FocusScope.of(context).requestFocus(_focusNode);
- }
- activeDollarGrid = active;
- });
- };
- update(change);
- },
- active: activeDollarGrid),
- Container(
- width: 20.0,
- )
- ],
- mainAxisAlignment: MainAxisAlignment.end,
- ),
- Container(),
- ],
+ ToggleButton(
+ activeWidget: const Padding(
+ padding: EdgeInsets.only(bottom: 5.0),
+ child: Text(
+ '@',
+ style: TextStyle(
+ color: Colors.orange,
+ fontWeight: FontWeight.bold,
+ fontSize: 20.0,
+ ),
+ ),
+ ),
+ unActiveWidget: const Padding(
+ padding: EdgeInsets.only(bottom: 5.0),
+ child: Text(
+ '@',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 20.0),
+ ),
+ ),
+ activeChanged: (bool active) {
+ onToolbarButtonActiveChanged(
+ keyboardHeight, active, () {
+ activeAtGrid = active;
+ });
+ },
+ active: activeAtGrid),
+ ToggleButton(
+ activeWidget: const Icon(
+ Icons.attach_money,
+ color: Colors.orange,
+ ),
+ unActiveWidget: const Icon(Icons.attach_money),
+ activeChanged: (bool active) {
+ onToolbarButtonActiveChanged(
+ keyboardHeight, active, () {
+ activeDollarGrid = active;
+ });
+ },
+ active: activeDollarGrid),
+ Container(
+ width: 20.0,
+ )
+ ],
+ mainAxisAlignment: MainAxisAlignment.end,
+ ),
+ Container(),
+ ],
+ ),
+ );
+ },
),
- ),
- Container(
- height: 2.0,
- color: Colors.blue,
- ),
- Container(
- height: showCustomKeyBoard ? _keyboardHeight : 0.0,
- child: buildCustomKeyBoard(),
- )
- ],
+
+ Container(
+ height: 2.0,
+ color: Colors.blue,
+ ),
+ StreamBuilder(
+ stream: _gridBuilderController.stream,
+ builder: (BuildContext b, AsyncSnapshot d) {
+ return SizedBox(
+ height: showCustomKeyBoard
+ ? _keyboardHeight -
+ (Platform.isIOS ? mediaQueryData.padding.bottom : 0)
+ : 0,
+ child: buildCustomKeyBoard());
+ },
+ ),
+
+ StreamBuilder(
+ stream: _gridBuilderController.stream,
+ builder: (BuildContext b, AsyncSnapshot d) {
+ return Container(
+ height: showCustomKeyBoard ? 0 : keyboardHeight,
+ );
+ },
+ ),
+ ],
+ ),
),
);
}
- void update(Function change) {
- if (showCustomKeyBoard) {
- change();
- } else {
- SystemChannels.textInput
- .invokeMethod('TextInput.hide')
- .whenComplete(() {
- Future.delayed(const Duration(milliseconds: 200))
- .whenComplete(() {
- change();
- });
- });
+ void onToolbarButtonActiveChanged(
+ double keyboardHeight, bool active, Function activeOne) {
+ if (keyboardHeight > 0) {
+ // make sure grid height = keyboardHeight
+ _keyboardHeight = keyboardHeight;
+ SystemChannels.textInput.invokeMethod('TextInput.hide');
+ }
+
+ if (active) {
+ activeDollarGrid = activeEmojiGird = activeAtGrid = false;
}
+
+ activeOne();
+ //activeDollarGrid = active;
+
+ _gridBuilderController.add(null);
}
Widget buildCustomKeyBoard() {
@@ -289,7 +327,7 @@ class _TextDemoState extends State {
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
child:
- Image.asset(emoji.EmojiUitl.instance.emojiMap['[${index + 1}]']),
+ Image.asset(emoji.EmojiUitl.instance.emojiMap['[${index + 1}]']!),
behavior: HitTestBehavior.translucent,
onTap: () {
insertText('[${index + 1}]');
@@ -372,6 +410,10 @@ class _TextDemoState extends State {
selection:
TextSelection.fromPosition(TextPosition(offset: text.length)));
}
+
+ SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
+ _key.currentState?.bringIntoView(_textEditingController.selection.base);
+ });
}
void manualDelete() {
diff --git a/example/lib/pages/main_page.dart b/example/lib/pages/main_page.dart
index 36fa184..eb077b4 100644
--- a/example/lib/pages/main_page.dart
+++ b/example/lib/pages/main_page.dart
@@ -1,8 +1,10 @@
+import 'package:collection/collection.dart';
import 'package:example/example_routes.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:ff_annotation_route/ff_annotation_route.dart';
import 'package:url_launcher/url_launcher.dart';
-import 'package:collection/collection.dart';
+
import '../example_route.dart';
import '../example_routes.dart' as example_routes;
@@ -16,18 +18,18 @@ class MainPage extends StatelessWidget {
routeNames.addAll(example_routes.routeNames);
routeNames.remove(Routes.fluttercandiesMainpage);
routeNames.remove(Routes.fluttercandiesDemogrouppage);
- routesGroup.addAll(groupBy(
+ routesGroup.addAll(groupBy(
routeNames
- .map((String name) => getRouteResult(name: name))
- .where((RouteResult element) => element.exts != null)
- .map((RouteResult e) => DemoRouteResult(e))
+ .map((String name) => getRouteSettings(name: name))
+ .where((FFRouteSettings element) => element.exts != null)
+ .map((FFRouteSettings e) => DemoRouteResult(e))
.toList()
- ..sort((DemoRouteResult a, DemoRouteResult b) =>
- b.group.compareTo(a.group)),
+ ..sort((DemoRouteResult a, DemoRouteResult b) =>
+ b.group!.compareTo(a.group!)),
(DemoRouteResult x) => x.group));
}
- final Map> routesGroup =
- >{};
+ final Map> routesGroup =
+ >{};
@override
Widget build(BuildContext context) {
@@ -40,7 +42,7 @@ class MainPage extends StatelessWidget {
ButtonTheme(
minWidth: 0.0,
padding: const EdgeInsets.symmetric(horizontal: 10.0),
- child: FlatButton(
+ child: TextButton(
child: const Text(
'Github',
style: TextStyle(
@@ -50,27 +52,29 @@ class MainPage extends StatelessWidget {
),
),
onPressed: () {
- launch('https://github.com/fluttercandies/extended_text_field');
+ launchUrl(Uri.parse(
+ 'https://github.com/fluttercandies/extended_text_field'));
},
),
),
- ButtonTheme(
- padding: const EdgeInsets.only(right: 10.0),
- minWidth: 0.0,
- child: FlatButton(
- child:
- Image.network('https://pub.idqqimg.com/wpa/images/group.png'),
- onPressed: () {
- launch('https://jq.qq.com/?_wv=1027&k=5bcc0gy');
- },
- ),
- )
+ if (!kIsWeb)
+ ButtonTheme(
+ padding: const EdgeInsets.only(right: 10.0),
+ minWidth: 0.0,
+ child: TextButton(
+ child: Image.network(
+ 'https://pub.idqqimg.com/wpa/images/group.png'),
+ onPressed: () {
+ launchUrl(Uri.parse('https://jq.qq.com/?_wv=1027&k=5bcc0gy'));
+ },
+ ),
+ )
],
),
body: ListView.builder(
itemBuilder: (BuildContext c, int index) {
// final RouteResult page = routes[index];
- final String type = routesGroup.keys.toList()[index];
+ final String type = routesGroup.keys.toList()[index]!;
return Container(
margin: const EdgeInsets.all(20.0),
child: GestureDetector(
@@ -91,10 +95,12 @@ class MainPage extends StatelessWidget {
),
onTap: () {
Navigator.pushNamed(
- context, Routes.fluttercandiesDemogrouppage,
- arguments: {
- 'keyValue': routesGroup.entries.toList()[index],
- });
+ context,
+ Routes.fluttercandiesDemogrouppage,
+ arguments: {
+ 'keyValue': routesGroup.entries.toList()[index],
+ },
+ );
},
));
},
@@ -109,11 +115,11 @@ class MainPage extends StatelessWidget {
routeName: 'DemoGroupPage',
)
class DemoGroupPage extends StatelessWidget {
- DemoGroupPage({MapEntry> keyValue})
+ DemoGroupPage({required MapEntry> keyValue})
: routes = keyValue.value
..sort((DemoRouteResult a, DemoRouteResult b) =>
- a.order.compareTo(b.order)),
- group = keyValue.key;
+ a.order!.compareTo(b.order!)),
+ group = keyValue.key!;
final List routes;
final String group;
@override
@@ -133,17 +139,17 @@ class DemoGroupPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
- (index + 1).toString() + '.' + page.routeResult.routeName,
+ (index + 1).toString() + '.' + page.routeResult.routeName!,
//style: TextStyle(inherit: false),
),
Text(
- page.routeResult.description,
+ page.routeResult.description!,
style: const TextStyle(color: Colors.grey),
)
],
),
onTap: () {
- Navigator.pushNamed(context, page.routeResult.name);
+ Navigator.pushNamed(context, page.routeResult.name!);
},
),
);
@@ -157,10 +163,10 @@ class DemoGroupPage extends StatelessWidget {
class DemoRouteResult {
DemoRouteResult(
this.routeResult,
- ) : order = routeResult.exts['order'] as int,
- group = routeResult.exts['group'] as String;
+ ) : order = routeResult.exts!['order'] as int?,
+ group = routeResult.exts!['group'] as String?;
- final int order;
- final String group;
- final RouteResult routeResult;
+ final int? order;
+ final String? group;
+ final FFRouteSettings routeResult;
}
diff --git a/example/lib/pages/simple/custom_toolbar.dart b/example/lib/pages/simple/custom_toolbar.dart
index aecd892..c57daf0 100644
--- a/example/lib/pages/simple/custom_toolbar.dart
+++ b/example/lib/pages/simple/custom_toolbar.dart
@@ -1,8 +1,10 @@
+// ignore_for_file: always_put_control_body_on_new_line
+
import 'package:example/special_text/my_extended_text_selection_controls.dart';
import 'package:example/special_text/my_special_text_span_builder.dart';
import 'package:extended_text_field/extended_text_field.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/material.dart';
-import 'package:ff_annotation_route/ff_annotation_route.dart';
///
/// create by zmtzawqlp on 2019/7/31
@@ -23,9 +25,8 @@ class CustomToolBar extends StatefulWidget {
}
class _CustomToolBarState extends State {
- final MyExtendedMaterialTextSelectionControls
- _myExtendedMaterialTextSelectionControls =
- MyExtendedMaterialTextSelectionControls();
+ final MyTextSelectionControls _myExtendedMaterialTextSelectionControls =
+ MyTextSelectionControls();
final MySpecialTextSpanBuilder _mySpecialTextSpanBuilder =
MySpecialTextSpanBuilder();
TextEditingController controller = TextEditingController()
@@ -44,13 +45,95 @@ class _CustomToolBarState extends State {
padding: const EdgeInsets.symmetric(horizontal: 30.0),
child: Center(
child: ExtendedTextField(
- textSelectionControls: _myExtendedMaterialTextSelectionControls,
+ selectionControls: _myExtendedMaterialTextSelectionControls,
specialTextSpanBuilder: _mySpecialTextSpanBuilder,
controller: controller,
maxLines: null,
+ // StrutStyle get strutStyle {
+ // if (_strutStyle == null) {
+ // return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
+ // }
+ // return _strutStyle!.inheritFromTextStyle(style);
+ // }
+ // default strutStyle is not good for WidgetSpan
+ strutStyle: const StrutStyle(),
+ shouldShowSelectionHandles: _shouldShowSelectionHandles,
+ textSelectionGestureDetectorBuilder: ({
+ required ExtendedTextSelectionGestureDetectorBuilderDelegate
+ delegate,
+ required Function showToolbar,
+ required Function hideToolbar,
+ required Function? onTap,
+ required BuildContext context,
+ required Function? requestKeyboard,
+ }) {
+ return MyCommonTextSelectionGestureDetectorBuilder(
+ delegate: delegate,
+ showToolbar: showToolbar,
+ hideToolbar: hideToolbar,
+ onTap: onTap,
+ context: context,
+ requestKeyboard: requestKeyboard,
+ );
+ },
),
),
),
);
}
+
+ bool _shouldShowSelectionHandles(
+ SelectionChangedCause? cause,
+ CommonTextSelectionGestureDetectorBuilder selectionGestureDetectorBuilder,
+ TextEditingValue editingValue,
+ ) {
+ // When the text field is activated by something that doesn't trigger the
+ // selection overlay, we shouldn't show the handles either.
+
+ //
+ // if (!selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
+ // return false;
+
+ if (cause == SelectionChangedCause.keyboard) return false;
+
+ // if (widget.readOnly && _effectiveController.selection.isCollapsed)
+ // return false;
+
+ // if (!_isEnabled) return false;
+
+ if (cause == SelectionChangedCause.longPress) return true;
+
+ if (editingValue.text.isNotEmpty) return true;
+
+ return false;
+ }
+}
+
+class MyCommonTextSelectionGestureDetectorBuilder
+ extends CommonTextSelectionGestureDetectorBuilder {
+ MyCommonTextSelectionGestureDetectorBuilder(
+ {required ExtendedTextSelectionGestureDetectorBuilderDelegate delegate,
+ required Function showToolbar,
+ required Function hideToolbar,
+ required Function? onTap,
+ required BuildContext context,
+ required Function? requestKeyboard})
+ : super(
+ delegate: delegate,
+ showToolbar: showToolbar,
+ hideToolbar: hideToolbar,
+ onTap: onTap,
+ context: context,
+ requestKeyboard: requestKeyboard,
+ );
+ @override
+ void onTapDown(TapDownDetails details) {
+ super.onTapDown(details);
+
+ /// always show toolbar
+ shouldShowSelectionToolbar = true;
+ }
+
+ @override
+ bool get showToolbarInWeb => true;
}
diff --git a/example/lib/pages/simple/no_keyboard.dart b/example/lib/pages/simple/no_keyboard.dart
new file mode 100644
index 0000000..52d7fb2
--- /dev/null
+++ b/example/lib/pages/simple/no_keyboard.dart
@@ -0,0 +1,428 @@
+import 'dart:ui' as ui;
+import 'package:extended_text_field/extended_text_field.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:oktoast/oktoast.dart';
+
+class CustomKeyboarBinding extends TextInputBinding {
+ @override
+ // ignore: unnecessary_overrides
+ bool ignoreTextInputShow() {
+ // you can override it base on your case
+ // if NoKeyboardFocusNode is not enough
+ return super.ignoreTextInputShow();
+ }
+}
+
+@FFRoute(
+ name: 'fluttercandies://NoKeyboard',
+ routeName: 'no system Keyboard',
+ description: 'show how to ignore system keyboard and show custom keyboard',
+ exts: {
+ 'group': 'Simple',
+ 'order': 2,
+ },
+)
+class NoSystemKeyboardDemo extends StatelessWidget {
+ const NoSystemKeyboardDemo({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('no system Keyboard'),
+ ),
+ body: GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ FocusManager.instance.primaryFocus?.unfocus();
+ },
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(children: const [
+ Text('ExtendedTextField'),
+ ExtendedTextFieldCase(),
+ Text('CustomTextField'),
+ TextFieldCase(),
+ ]),
+ ),
+ ),
+ );
+ }
+}
+
+class ExtendedTextFieldCase extends StatefulWidget {
+ const ExtendedTextFieldCase({Key? key}) : super(key: key);
+
+ @override
+ State createState() => _ExtendedTextFieldCaseState();
+}
+
+class _ExtendedTextFieldCaseState extends State
+ with CustomKeyboardShowStateMixin {
+ @override
+ Widget build(BuildContext context) {
+ return ExtendedTextField(
+ // you must use TextInputFocusNode
+ focusNode: _focusNode..debugLabel = 'ExtendedTextField',
+ // if your custom keyboard can be close without unfocus
+ // you can show custom keyboard when TextField onTap
+ controller: _controller,
+ maxLines: null,
+ inputFormatters: _inputFormatters,
+ );
+ }
+}
+
+class TextFieldCase extends StatefulWidget {
+ const TextFieldCase({Key? key}) : super(key: key);
+ @override
+ State createState() => TextFieldCaseState();
+}
+
+class TextFieldCaseState extends State
+ with CustomKeyboardShowStateMixin {
+ @override
+ Widget build(BuildContext context) {
+ return TextField(
+ // you must use TextInputFocusNode
+ focusNode: _focusNode..debugLabel = 'CustomTextField',
+ // if your custom keyboard can be close without unfocus
+ // you can show custom keyboard when TextField onTap
+ onTap: _onTextFiledTap,
+ controller: _controller,
+ inputFormatters: _inputFormatters,
+ maxLines: null,
+ );
+ }
+}
+
+@optionalTypeArgs
+mixin CustomKeyboardShowStateMixin on State {
+ final TextInputFocusNode _focusNode = TextInputFocusNode();
+ final TextEditingController _controller = TextEditingController();
+ PersistentBottomSheetController? _bottomSheetController;
+
+ final List _inputFormatters = [
+ // digit or decimal
+ FilteringTextInputFormatter.allow(RegExp(r'[1-9]{1}[0-9.]*')),
+ // only one decimal
+ TextInputFormatter.withFunction(
+ (TextEditingValue oldValue, TextEditingValue newValue) =>
+ newValue.text.indexOf('.') != newValue.text.lastIndexOf('.')
+ ? oldValue
+ : newValue),
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+ _focusNode.addListener(_handleFocusChanged);
+ }
+
+ void _onTextFiledTap() {
+ if (_bottomSheetController == null) {
+ _handleFocusChanged();
+ }
+ }
+
+ void _handleFocusChanged() {
+ if (_focusNode.hasFocus) {
+ // just demo, you can define your custom keyboard as you want
+ _bottomSheetController = showBottomSheet(
+ context: FocusManager.instance.primaryFocus!.context!,
+ // set false, if don't want to drag to close custom keyboard
+ enableDrag: true,
+ builder: (BuildContext b) {
+ return Material(
+ //shadowColor: Colors.grey,
+ color: Colors.grey.withOpacity(0.3),
+ //elevation: 8,
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: 10,
+ right: 10,
+ top: 20,
+ bottom:
+ ui.window.viewPadding.bottom / ui.window.devicePixelRatio,
+ ),
+ child: IntrinsicHeight(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Expanded(
+ flex: 15,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Row(
+ children: [
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 1,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 2,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 3,
+ insertText: insertText,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 4,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 5,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 6,
+ insertText: insertText,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 7,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 8,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 9,
+ insertText: insertText,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(
+ flex: 5,
+ child: CustomButton(
+ child: const Text('.'),
+ onTap: () {
+ insertText('.');
+ },
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: NumberButton(
+ number: 0,
+ insertText: insertText,
+ ),
+ ),
+ Expanded(
+ flex: 5,
+ child: CustomButton(
+ child: const Icon(Icons.arrow_downward),
+ onTap: () {
+ _focusNode.unfocus();
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ flex: 7,
+ child: Column(
+ mainAxisSize: MainAxisSize.max,
+ children: [
+ Expanded(
+ child: CustomButton(
+ child: const Icon(Icons.backspace),
+ onTap: () {
+ manualDelete();
+ },
+ )),
+ Expanded(
+ child: CustomButton(
+ child: const Icon(Icons.keyboard_return),
+ onTap: () {
+ showToast('onSubmitted: ${_controller.text}');
+ },
+ ))
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ });
+ // maybe drag close
+ _bottomSheetController?.closed.whenComplete(() {
+ _bottomSheetController = null;
+ });
+ } else {
+ _bottomSheetController?.close();
+ _bottomSheetController = null;
+ }
+ }
+
+ @override
+ void dispose() {
+ _focusNode.removeListener(_handleFocusChanged);
+ super.dispose();
+ }
+
+ void insertText(String text) {
+ final TextEditingValue oldValue = _controller.value;
+ TextEditingValue newValue = oldValue;
+ final int start = oldValue.selection.baseOffset;
+ int end = oldValue.selection.extentOffset;
+ if (oldValue.selection.isValid) {
+ String newText = '';
+ if (oldValue.selection.isCollapsed) {
+ if (end > 0) {
+ newText += oldValue.text.substring(0, end);
+ }
+ newText += text;
+ if (oldValue.text.length > end) {
+ newText += oldValue.text.substring(end, oldValue.text.length);
+ }
+ } else {
+ newText = oldValue.text.replaceRange(start, end, text);
+ end = start;
+ }
+
+ newValue = oldValue.copyWith(
+ text: newText,
+ selection: oldValue.selection.copyWith(
+ baseOffset: end + text.length, extentOffset: end + text.length));
+ } else {
+ newValue = TextEditingValue(
+ text: text,
+ selection:
+ TextSelection.fromPosition(TextPosition(offset: text.length)));
+ }
+ for (final TextInputFormatter inputFormatter in _inputFormatters) {
+ newValue = inputFormatter.formatEditUpdate(oldValue, newValue);
+ }
+
+ _controller.value = newValue;
+ }
+
+ void manualDelete() {
+ //delete by code
+ final TextEditingValue _value = _controller.value;
+ final TextSelection selection = _value.selection;
+ if (!selection.isValid) {
+ return;
+ }
+
+ TextEditingValue value;
+ final String actualText = _value.text;
+ if (selection.isCollapsed && selection.start == 0) {
+ return;
+ }
+ final int start =
+ selection.isCollapsed ? selection.start - 1 : selection.start;
+ final int end = selection.end;
+
+ value = TextEditingValue(
+ text: actualText.replaceRange(start, end, ''),
+ selection: TextSelection.collapsed(offset: start),
+ );
+
+ _controller.value = value;
+ }
+}
+
+class NumberButton extends StatelessWidget {
+ const NumberButton({
+ Key? key,
+ required this.number,
+ required this.insertText,
+ }) : super(key: key);
+ final int number;
+ final Function(String text) insertText;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: () {
+ insertText('$number');
+ },
+ child: Container(
+ margin: const EdgeInsets.all(5),
+ alignment: Alignment.center,
+ height: 50,
+ child: Text(
+ '$number',
+ ),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ );
+ }
+}
+
+class CustomButton extends StatelessWidget {
+ const CustomButton({
+ Key? key,
+ required this.child,
+ required this.onTap,
+ }) : super(key: key);
+ final Widget child;
+ final GestureTapCallback onTap;
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Container(
+ margin: const EdgeInsets.all(5),
+ alignment: Alignment.center,
+ height: 50,
+ child: child,
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey),
+ borderRadius: BorderRadius.circular(8),
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/lib/pages/simple/widget_span.dart b/example/lib/pages/simple/widget_span.dart
index 7bd7afb..b60b5c4 100644
--- a/example/lib/pages/simple/widget_span.dart
+++ b/example/lib/pages/simple/widget_span.dart
@@ -1,8 +1,8 @@
import 'package:example/special_text/email_span_builder.dart';
import 'package:example/special_text/my_special_text_span_builder.dart';
import 'package:extended_text_field/extended_text_field.dart';
+import 'package:ff_annotation_route_library/ff_annotation_route_library.dart';
import 'package:flutter/material.dart';
-import 'package:ff_annotation_route/ff_annotation_route.dart';
///
/// create by zmtzawqlp on 2019/8/4
@@ -29,7 +29,7 @@ class _WidgetSpanDemoState extends State {
'[33]Extended text field help you to build rich text quickly. any special text you will have with extended text field. this is demo to show how to create special text with widget span.'
'\n\nIt\'s my pleasure to invite you to join \$FlutterCandies\$ if you want to improve flutter .[36]'
'\n\nif you meet any problem, please let me konw @zmtzawqlp .[44]';
- EmailSpanBuilder _emailSpanBuilder;
+ EmailSpanBuilder? _emailSpanBuilder;
@override
void initState() {
_emailSpanBuilder = EmailSpanBuilder(controller, context);
@@ -64,6 +64,14 @@ class _WidgetSpanDemoState extends State {
controller: controller,
specialTextSpanBuilder: _emailSpanBuilder,
maxLines: null,
+ // StrutStyle get strutStyle {
+ // if (_strutStyle == null) {
+ // return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
+ // }
+ // return _strutStyle!.inheritFromTextStyle(style);
+ // }
+ // default strutStyle is not good for WidgetSpan
+ strutStyle: const StrutStyle(),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: const Icon(Icons.add),
@@ -85,7 +93,7 @@ class _WidgetSpanDemoState extends State {
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
- FlatButton(
+ TextButton(
onPressed: () {
insertEmail(
'zmtzawqlp@live.com ',
@@ -94,7 +102,7 @@ class _WidgetSpanDemoState extends State {
},
child: const Text(
'zmtzawqlp@live.com')),
- FlatButton(
+ TextButton(
onPressed: () {
insertEmail(
'410496936@qq.com ',
diff --git a/example/lib/special_text/at_text.dart b/example/lib/special_text/at_text.dart
index ff9dc3c..d80ab3e 100644
--- a/example/lib/special_text/at_text.dart
+++ b/example/lib/special_text/at_text.dart
@@ -3,18 +3,18 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class AtText extends SpecialText {
- AtText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
+ AtText(TextStyle? textStyle, SpecialTextGestureTapCallback? onTap,
{this.showAtBackground = false, this.start})
: super(flag, ' ', textStyle, onTap: onTap);
static const String flag = '@';
- final int start;
+ final int? start;
/// whether show background for @somebody
final bool showAtBackground;
@override
InlineSpan finishText() {
- final TextStyle textStyle =
+ final TextStyle? textStyle =
this.textStyle?.copyWith(color: Colors.blue, fontSize: 16.0);
final String atText = toString();
@@ -24,7 +24,7 @@ class AtText extends SpecialText {
background: Paint()..color = Colors.blue.withOpacity(0.15),
text: atText,
actualText: atText,
- start: start,
+ start: start!,
///caret can move into special text
deleteAll: true,
@@ -32,18 +32,18 @@ class AtText extends SpecialText {
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) {
- onTap(atText);
+ onTap!(atText);
}
}))
: SpecialTextSpan(
text: atText,
actualText: atText,
- start: start,
+ start: start!,
style: textStyle,
recognizer: (TapGestureRecognizer()
..onTap = () {
if (onTap != null) {
- onTap(atText);
+ onTap!(atText);
}
}));
}
diff --git a/example/lib/special_text/dollar_text.dart b/example/lib/special_text/dollar_text.dart
index 8698ed5..989feae 100644
--- a/example/lib/special_text/dollar_text.dart
+++ b/example/lib/special_text/dollar_text.dart
@@ -3,11 +3,11 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class DollarText extends SpecialText {
- DollarText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
+ DollarText(TextStyle? textStyle, SpecialTextGestureTapCallback? onTap,
{this.start})
: super(flag, flag, textStyle, onTap: onTap);
static const String flag = '\$';
- final int start;
+ final int? start;
@override
InlineSpan finishText() {
final String text = getContent();
@@ -15,7 +15,7 @@ class DollarText extends SpecialText {
return SpecialTextSpan(
text: text,
actualText: toString(),
- start: start,
+ start: start!,
///caret can move into special text
deleteAll: true,
@@ -23,7 +23,7 @@ class DollarText extends SpecialText {
recognizer: TapGestureRecognizer()
..onTap = () {
if (onTap != null) {
- onTap(toString());
+ onTap!(toString());
}
});
}
diff --git a/example/lib/special_text/email_span_builder.dart b/example/lib/special_text/email_span_builder.dart
index 88b2092..06aab1c 100644
--- a/example/lib/special_text/email_span_builder.dart
+++ b/example/lib/special_text/email_span_builder.dart
@@ -12,14 +12,16 @@ class EmailSpanBuilder extends SpecialTextSpanBuilder {
final TextEditingController controller;
final BuildContext context;
@override
- SpecialText createSpecialText(String flag,
- {TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {
- if (flag == null || flag == '') {
+ SpecialText? createSpecialText(String flag,
+ {TextStyle? textStyle,
+ SpecialTextGestureTapCallback? onTap,
+ int? index}) {
+ if (flag == '') {
return null;
}
if (!flag.startsWith(' ') && !flag.startsWith('@')) {
- return EmailText(textStyle, onTap,
+ return EmailText(textStyle!, onTap,
start: index,
context: context,
controller: controller,
diff --git a/example/lib/special_text/email_text.dart b/example/lib/special_text/email_text.dart
index fa9e718..47c47cb 100644
--- a/example/lib/special_text/email_text.dart
+++ b/example/lib/special_text/email_text.dart
@@ -3,12 +3,12 @@ import 'package:extended_text_library/extended_text_library.dart';
import 'package:flutter/material.dart';
class EmailText extends SpecialText {
- EmailText(TextStyle textStyle, SpecialTextGestureTapCallback onTap,
- {this.start, this.controller, this.context, String startFlag})
+ EmailText(TextStyle textStyle, SpecialTextGestureTapCallback? onTap,
+ {this.start, this.controller, this.context, required String startFlag})
: super(startFlag, ' ', textStyle, onTap: onTap);
- final TextEditingController controller;
- final int start;
- final BuildContext context;
+ final TextEditingController? controller;
+ final int? start;
+ final BuildContext? context;
@override
bool isEnd(String value) {
final int index = value.indexOf('@');
@@ -26,7 +26,7 @@ class EmailText extends SpecialText {
return ExtendedWidgetSpan(
actualText: text,
- start: start,
+ start: start!,
alignment: ui.PlaceholderAlignment.middle,
child: GestureDetector(
child: Padding(
@@ -53,11 +53,11 @@ class EmailText extends SpecialText {
size: 15.0,
),
onTap: () {
- controller.value = controller.value.copyWith(
- text: controller.text
- .replaceRange(start, start + text.length, ''),
+ controller!.value = controller!.value.copyWith(
+ text: controller!.text
+ .replaceRange(start!, start! + text.length, ''),
selection: TextSelection.fromPosition(
- TextPosition(offset: start)));
+ TextPosition(offset: start!)));
},
)
],
@@ -66,7 +66,7 @@ class EmailText extends SpecialText {
),
onTap: () {
showDialog(
- context: context,
+ context: context!,
barrierDismissible: true,
builder: (BuildContext c) {
final TextEditingController textEditingController =
@@ -82,21 +82,21 @@ class EmailText extends SpecialText {
child: TextField(
controller: textEditingController,
decoration: InputDecoration(
- suffixIcon: FlatButton(
+ suffixIcon: TextButton(
child: const Text('OK'),
onPressed: () {
- controller.value = controller.value.copyWith(
- text: controller.text.replaceRange(
- start,
- start + text.length,
+ controller!.value = controller!.value.copyWith(
+ text: controller!.text.replaceRange(
+ start!,
+ start! + text.length,
textEditingController.text + ' '),
selection: TextSelection.fromPosition(
TextPosition(
- offset: start +
+ offset: start! +
(textEditingController.text + ' ')
.length)));
- Navigator.pop(context);
+ Navigator.pop(context!);
},
)),
),
diff --git a/example/lib/special_text/emoji_text.dart b/example/lib/special_text/emoji_text.dart
index 057d66d..6fc483b 100644
--- a/example/lib/special_text/emoji_text.dart
+++ b/example/lib/special_text/emoji_text.dart
@@ -1,36 +1,34 @@
import 'package:extended_text_library/extended_text_library.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
///emoji/image text
class EmojiText extends SpecialText {
- EmojiText(TextStyle textStyle, {this.start})
+ EmojiText(TextStyle? textStyle, {this.start})
: super(EmojiText.flag, ']', textStyle);
static const String flag = '[';
- final int start;
+ final int? start;
@override
InlineSpan finishText() {
final String key = toString();
- ///https://github.com/flutter/flutter/issues/42086
- /// widget span is not working on web
- if (EmojiUitl.instance.emojiMap.containsKey(key) && !kIsWeb) {
- //fontsize id define image height
- //size = 30.0/26.0 * fontSize
- const double size = 20.0;
+ if (EmojiUitl.instance.emojiMap.containsKey(key)) {
+ double size = 18;
+
+ final TextStyle ts = textStyle!;
+ if (ts.fontSize != null) {
+ size = ts.fontSize! * 1.15;
+ }
- ///fontSize 26 and text height =30.0
- //final double fontSize = 26.0;
return ImageSpan(
AssetImage(
- EmojiUitl.instance.emojiMap[key],
+ EmojiUitl.instance.emojiMap[key]!,
),
actualText: key,
imageWidth: size,
imageHeight: size,
- start: start,
- fit: BoxFit.fill,
- margin: const EdgeInsets.only(left: 2.0, top: 2.0, right: 2.0));
+ start: start!,
+ //fit: BoxFit.fill,
+ margin: const EdgeInsets.all(2));
}
return TextSpan(text: toString(), style: textStyle);
@@ -50,6 +48,6 @@ class EmojiUitl {
final String _emojiFilePath = 'assets';
- static EmojiUitl _instance;
+ static EmojiUitl? _instance;
static EmojiUitl get instance => _instance ??= EmojiUitl._();
}
diff --git a/example/lib/special_text/image_text.dart b/example/lib/special_text/image_text.dart
index 0a1f0cd..dd2355a 100644
--- a/example/lib/special_text/image_text.dart
+++ b/example/lib/special_text/image_text.dart
@@ -6,8 +6,8 @@ import 'package:html/dom.dart' hide Text;
import 'package:html/parser.dart';
class ImageText extends SpecialText {
- ImageText(TextStyle textStyle,
- {this.start, SpecialTextGestureTapCallback onTap})
+ ImageText(TextStyle? textStyle,
+ {this.start, SpecialTextGestureTapCallback? onTap})
: super(
ImageText.flag,
'/>',
@@ -16,9 +16,9 @@ class ImageText extends SpecialText {
);
static const String flag = ' _imageUrl;
+ final int? start;
+ String? _imageUrl;
+ String? get imageUrl => _imageUrl;
@override
InlineSpan finishText() {
///content already has endflag '/'
@@ -34,13 +34,13 @@ class ImageText extends SpecialText {
final Document html = parse(text);
final Element img = html.getElementsByTagName('img').first;
- final String url = img.attributes['src'];
+ final String url = img.attributes['src']!;
_imageUrl = url;
//fontsize id define image height
//size = 30.0/26.0 * fontSize
- double width = 60.0;
- double height = 60.0;
+ double? width = 60.0;
+ double? height = 60.0;
const BoxFit fit = BoxFit.cover;
const double num300 = 60.0;
const double num400 = 80.0;
@@ -49,9 +49,9 @@ class ImageText extends SpecialText {
width = num400;
const bool knowImageSize = true;
if (knowImageSize) {
- height = double.tryParse(img.attributes['height']);
- width = double.tryParse(img.attributes['width']);
- final double n = height / width;
+ height = double.tryParse(img.attributes['height']!);
+ width = double.tryParse(img.attributes['width']!);
+ final double n = height! / width!;
if (n >= 4 / 3) {
width = num300;
height = num400;
@@ -69,7 +69,7 @@ class ImageText extends SpecialText {
//final double fontSize = 26.0;
return ExtendedWidgetSpan(
- start: start,
+ start: start!,
actualText: text,
child: GestureDetector(
onTap: () {
diff --git a/example/lib/special_text/my_extended_text_selection_controls.dart b/example/lib/special_text/my_extended_text_selection_controls.dart
index daa418b..b0aae79 100644
--- a/example/lib/special_text/my_extended_text_selection_controls.dart
+++ b/example/lib/special_text/my_extended_text_selection_controls.dart
@@ -1,25 +1,25 @@
import 'dart:math' as math;
-import 'package:extended_text_library/extended_text_library.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
import 'package:url_launcher/url_launcher.dart';
-// Minimal padding from all edges of the selection toolbar to all edges of the
-// viewport.
+///
+/// create by zmtzawqlp on 2019/8/3
+///
+
const double _kHandleSize = 22.0;
-const double _kToolbarScreenPadding = 8.0;
-const double _kToolbarHeight = 44.0;
-// Padding when positioning toolbar below selection.
+
+// Padding between the toolbar and the anchor.
const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
const double _kToolbarContentDistance = 8.0;
-///
-/// create by zmtzawqlp on 2019/8/3
-///
+/// Android Material styled text selection controls.
+class MyTextSelectionControls extends TextSelectionControls {
+ /// Returns the size of the Material handle.
+ @override
+ Size getHandleSize(double textLineHeight) =>
+ const Size(_kHandleSize, _kHandleSize);
-class MyExtendedMaterialTextSelectionControls
- extends ExtendedMaterialTextSelectionControls {
- MyExtendedMaterialTextSelectionControls();
+ /// Builder for material-style copy/paste text selection toolbar.
@override
Widget buildToolbar(
BuildContext context,
@@ -28,72 +28,39 @@ class MyExtendedMaterialTextSelectionControls
Offset selectionMidpoint,
List endpoints,
TextSelectionDelegate delegate,
- ClipboardStatusNotifier clipboardStatus,
+ ClipboardStatusNotifier? clipboardStatus,
+ Offset? lastSecondaryTapDownPosition,
) {
- assert(debugCheckHasMediaQuery(context));
- assert(debugCheckHasMaterialLocalizations(context));
-
- // The toolbar should appear below the TextField when there is not enough
- // space above the TextField to show it.
- final TextSelectionPoint startTextSelectionPoint = endpoints[0];
- final TextSelectionPoint endTextSelectionPoint =
- endpoints.length > 1 ? endpoints[1] : endpoints[0];
- const double closedToolbarHeightNeeded =
- _kToolbarScreenPadding + _kToolbarHeight + _kToolbarContentDistance;
- final double paddingTop = MediaQuery.of(context).padding.top;
- final double availableHeight = globalEditableRegion.top +
- startTextSelectionPoint.point.dy -
- textLineHeight -
- paddingTop;
- final bool fitsAbove = closedToolbarHeightNeeded <= availableHeight;
- final Offset anchor = Offset(
- globalEditableRegion.left + selectionMidpoint.dx,
- fitsAbove
- ? globalEditableRegion.top +
- startTextSelectionPoint.point.dy -
- textLineHeight -
- _kToolbarContentDistance
- : globalEditableRegion.top +
- endTextSelectionPoint.point.dy +
- _kToolbarContentDistanceBelow,
- );
-
- return Stack(
- children: [
- CustomSingleChildLayout(
- delegate: ExtendedMaterialTextSelectionToolbarLayout(
- anchor,
- _kToolbarScreenPadding + paddingTop,
- fitsAbove,
- ),
- child: _TextSelectionToolbar(
- handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
- handleCopy: canCopy(delegate)
- ? () => handleCopy(delegate, clipboardStatus)
- : null,
- handlePaste:
- canPaste(delegate) ? () => handlePaste(delegate) : null,
- handleSelectAll:
- canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
- handleLike: () {
- //mailto:?subject=&body=, e.g.
- launch(
- 'mailto:zmtzawqlp@live.com?subject=extended_text_share&body=${delegate.textEditingValue.text}');
- delegate.hideToolbar();
- //clear selecction
- delegate.textEditingValue = delegate.textEditingValue.copyWith(
- selection: TextSelection.collapsed(
- offset: delegate.textEditingValue.selection.end));
- },
- ),
- ),
- ],
+ return _TextSelectionControlsToolbar(
+ globalEditableRegion: globalEditableRegion,
+ textLineHeight: textLineHeight,
+ selectionMidpoint: selectionMidpoint,
+ endpoints: endpoints,
+ delegate: delegate,
+ clipboardStatus: clipboardStatus,
+ handleCut: canCut(delegate) ? () => handleCut(delegate, null) : null,
+ handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
+ handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
+ handleSelectAll:
+ canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
+ handleLike: () {
+ launchUrl(Uri.parse(
+ 'mailto:zmtzawqlp@live.com?subject=extended_text_share&body=${delegate.textEditingValue.text}'));
+ delegate.hideToolbar();
+ delegate.userUpdateTextEditingValue(
+ delegate.textEditingValue
+ .copyWith(selection: const TextSelection.collapsed(offset: 0)),
+ SelectionChangedCause.toolbar,
+ );
+ },
);
}
+ /// Builder for material-style text selection handles.
@override
Widget buildHandle(
- BuildContext context, TextSelectionHandleType type, double textHeight) {
+ BuildContext context, TextSelectionHandleType type, double textLineHeight,
+ [VoidCallback? onTap, double? startGlyphHeight, double? endGlyphHeight]) {
final Widget handle = SizedBox(
width: _kHandleSize,
height: _kHandleSize,
@@ -119,68 +86,198 @@ class MyExtendedMaterialTextSelectionControls
case TextSelectionHandleType.collapsed: // points up
return handle;
}
- assert(type != null);
- return null;
}
+
+ /// Gets anchor for material-style text selection handles.
+ ///
+ /// See [TextSelectionControls.getHandleAnchor].
+ @override
+ Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight,
+ [double? startGlyphHeight, double? endGlyphHeight]) {
+ switch (type) {
+ case TextSelectionHandleType.left:
+ return const Offset(_kHandleSize, 0);
+ case TextSelectionHandleType.right:
+ return Offset.zero;
+ default:
+ return const Offset(_kHandleSize / 2, -4);
+ }
+ }
+
+ @override
+ bool canSelectAll(TextSelectionDelegate delegate) {
+ // Android allows SelectAll when selection is not collapsed, unless
+ // everything has already been selected.
+ final TextEditingValue value = delegate.textEditingValue;
+ return delegate.selectAllEnabled &&
+ value.text.isNotEmpty &&
+ !(value.selection.start == 0 &&
+ value.selection.end == value.text.length);
+ }
+}
+
+// The label and callback for the available default text selection menu buttons.
+class _TextSelectionToolbarItemData {
+ const _TextSelectionToolbarItemData({
+ required this.label,
+ required this.onPressed,
+ });
+
+ final String label;
+ final VoidCallback? onPressed;
}
-/// Manages a copy/paste text selection toolbar.
-class _TextSelectionToolbar extends StatelessWidget {
- const _TextSelectionToolbar({
- Key key,
- this.handleCopy,
- this.handleSelectAll,
- this.handleCut,
- this.handlePaste,
- this.handleLike,
- }) : super(key: key);
-
- final VoidCallback handleCut;
- final VoidCallback handleCopy;
- final VoidCallback handlePaste;
- final VoidCallback handleSelectAll;
- final VoidCallback handleLike;
+// The highest level toolbar widget, built directly by buildToolbar.
+class _TextSelectionControlsToolbar extends StatefulWidget {
+ const _TextSelectionControlsToolbar({
+ required this.clipboardStatus,
+ required this.delegate,
+ required this.endpoints,
+ required this.globalEditableRegion,
+ required this.handleCut,
+ required this.handleCopy,
+ required this.handlePaste,
+ required this.handleSelectAll,
+ required this.selectionMidpoint,
+ required this.textLineHeight,
+ required this.handleLike,
+ });
+
+ final ClipboardStatusNotifier? clipboardStatus;
+ final TextSelectionDelegate delegate;
+ final List endpoints;
+ final Rect globalEditableRegion;
+ final VoidCallback? handleCut;
+ final VoidCallback? handleCopy;
+ final VoidCallback? handlePaste;
+ final VoidCallback? handleSelectAll;
+ final VoidCallback? handleLike;
+ final Offset selectionMidpoint;
+ final double textLineHeight;
@override
- Widget build(BuildContext context) {
- final List items = [];
- final MaterialLocalizations localizations =
- MaterialLocalizations.of(context);
+ _TextSelectionControlsToolbarState createState() =>
+ _TextSelectionControlsToolbarState();
+}
- if (handleCut != null) {
- items.add(FlatButton(
- child: Text(localizations.cutButtonLabel), onPressed: handleCut));
- }
- if (handleCopy != null) {
- items.add(FlatButton(
- child: Text(localizations.copyButtonLabel), onPressed: handleCopy));
+class _TextSelectionControlsToolbarState
+ extends State<_TextSelectionControlsToolbar> with TickerProviderStateMixin {
+ void _onChangedClipboardStatus() {
+ setState(() {
+ // Inform the widget that the value of clipboardStatus has changed.
+ });
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
+ }
+
+ @override
+ void didUpdateWidget(_TextSelectionControlsToolbar oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.clipboardStatus != oldWidget.clipboardStatus) {
+ widget.clipboardStatus?.addListener(_onChangedClipboardStatus);
+ oldWidget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
}
- if (handlePaste != null) {
- items.add(FlatButton(
- child: Text(localizations.pasteButtonLabel),
- onPressed: handlePaste,
- ));
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ widget.clipboardStatus?.removeListener(_onChangedClipboardStatus);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // If there are no buttons to be shown, don't render anything.
+ if (widget.handleCut == null &&
+ widget.handleCopy == null &&
+ widget.handlePaste == null &&
+ widget.handleSelectAll == null) {
+ return const SizedBox.shrink();
}
- if (handleSelectAll != null) {
- items.add(FlatButton(
- child: Text(localizations.selectAllButtonLabel),
- onPressed: handleSelectAll));
+ // If the paste button is desired, don't render anything until the state of
+ // the clipboard is known, since it's used to determine if paste is shown.
+ if (widget.handlePaste != null &&
+ widget.clipboardStatus?.value == ClipboardStatus.unknown) {
+ return const SizedBox.shrink();
}
- if (handleLike != null) {
- items.add(
- FlatButton(child: const Icon(Icons.favorite), onPressed: handleLike));
- }
+ // Calculate the positioning of the menu. It is placed above the selection
+ // if there is enough room, or otherwise below.
+ final TextSelectionPoint startTextSelectionPoint = widget.endpoints[0];
+ final TextSelectionPoint endTextSelectionPoint =
+ widget.endpoints.length > 1 ? widget.endpoints[1] : widget.endpoints[0];
+ final Offset anchorAbove = Offset(
+ widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
+ widget.globalEditableRegion.top +
+ startTextSelectionPoint.point.dy -
+ widget.textLineHeight -
+ _kToolbarContentDistance,
+ );
+ final Offset anchorBelow = Offset(
+ widget.globalEditableRegion.left + widget.selectionMidpoint.dx,
+ widget.globalEditableRegion.top +
+ endTextSelectionPoint.point.dy +
+ _kToolbarContentDistanceBelow,
+ );
+
+ // Determine which buttons will appear so that the order and total number is
+ // known. A button's position in the menu can slightly affect its
+ // appearance.
+ assert(debugCheckHasMaterialLocalizations(context));
+ final MaterialLocalizations localizations =
+ MaterialLocalizations.of(context);
+ final List<_TextSelectionToolbarItemData> itemDatas =
+ <_TextSelectionToolbarItemData>[
+ if (widget.handleCut != null)
+ _TextSelectionToolbarItemData(
+ label: localizations.cutButtonLabel,
+ onPressed: widget.handleCut!,
+ ),
+ if (widget.handleCopy != null)
+ _TextSelectionToolbarItemData(
+ label: localizations.copyButtonLabel,
+ onPressed: widget.handleCopy!,
+ ),
+ if (widget.handlePaste != null &&
+ widget.clipboardStatus?.value == ClipboardStatus.pasteable)
+ _TextSelectionToolbarItemData(
+ label: localizations.pasteButtonLabel,
+ onPressed: widget.handlePaste!,
+ ),
+ if (widget.handleSelectAll != null)
+ _TextSelectionToolbarItemData(
+ label: localizations.selectAllButtonLabel,
+ onPressed: widget.handleSelectAll!,
+ ),
+ _TextSelectionToolbarItemData(
+ label: 'Like',
+ onPressed: widget.handleLike,
+ ),
+ ];
// If there is no option available, build an empty widget.
- if (items.isEmpty) {
- return Container(width: 0.0, height: 0.0);
+ if (itemDatas.isEmpty) {
+ return const SizedBox(width: 0.0, height: 0.0);
}
- return Material(
- elevation: 1.0,
- child: Wrap(children: items),
- borderRadius: const BorderRadius.all(Radius.circular(10.0)),
+ return TextSelectionToolbar(
+ anchorAbove: anchorAbove,
+ anchorBelow: anchorBelow,
+ children: itemDatas
+ .asMap()
+ .entries
+ .map((MapEntry entry) {
+ return TextSelectionToolbarTextButton(
+ padding: TextSelectionToolbarTextButton.getPadding(
+ entry.key, itemDatas.length),
+ onPressed: entry.value.onPressed,
+ child: Text(entry.value.label),
+ );
+ }).toList(),
);
}
}
diff --git a/example/lib/special_text/my_special_text_span_builder.dart b/example/lib/special_text/my_special_text_span_builder.dart
index 9e96dc4..ae1bbe7 100644
--- a/example/lib/special_text/my_special_text_span_builder.dart
+++ b/example/lib/special_text/my_special_text_span_builder.dart
@@ -1,6 +1,5 @@
import 'package:example/special_text/image_text.dart';
import 'package:extended_text_library/extended_text_library.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'at_text.dart';
@@ -12,41 +11,34 @@ class MySpecialTextSpanBuilder extends SpecialTextSpanBuilder {
/// whether show background for @somebody
final bool showAtBackground;
- @override
- TextSpan build(String data,
- {TextStyle textStyle, SpecialTextGestureTapCallback onTap}) {
- if (kIsWeb) {
- return TextSpan(text: data, style: textStyle);
- }
-
- return super.build(data, textStyle: textStyle, onTap: onTap);
- }
@override
- SpecialText createSpecialText(String flag,
- {TextStyle textStyle, SpecialTextGestureTapCallback onTap, int index}) {
- if (flag == null || flag == '') {
+ SpecialText? createSpecialText(String flag,
+ {TextStyle? textStyle,
+ SpecialTextGestureTapCallback? onTap,
+ int? index}) {
+ if (flag == '') {
return null;
}
///index is end index of start flag, so text start index should be index-(flag.length-1)
if (isStart(flag, EmojiText.flag)) {
- return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));
+ return EmojiText(textStyle, start: index! - (EmojiText.flag.length - 1));
} else if (isStart(flag, ImageText.flag)) {
return ImageText(textStyle,
- start: index - (ImageText.flag.length - 1), onTap: onTap);
+ start: index! - (ImageText.flag.length - 1), onTap: onTap);
} else if (isStart(flag, AtText.flag)) {
return AtText(
textStyle,
onTap,
- start: index - (AtText.flag.length - 1),
+ start: index! - (AtText.flag.length - 1),
showAtBackground: showAtBackground,
);
} else if (isStart(flag, EmojiText.flag)) {
- return EmojiText(textStyle, start: index - (EmojiText.flag.length - 1));
+ return EmojiText(textStyle, start: index! - (EmojiText.flag.length - 1));
} else if (isStart(flag, DollarText.flag)) {
return DollarText(textStyle, onTap,
- start: index - (DollarText.flag.length - 1));
+ start: index! - (DollarText.flag.length - 1));
}
return null;
}
diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000..4b81f9b
--- /dev/null
+++ b/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000..5caa9d1
--- /dev/null
+++ b/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000..8236f57
--- /dev/null
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,12 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import url_launcher_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
+}
diff --git a/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig
new file mode 100644
index 0000000..2c25018
--- /dev/null
+++ b/example/macos/Flutter/ephemeral/Flutter-Generated.xcconfig
@@ -0,0 +1,14 @@
+// This is a generated file; do not edit or check into version control.
+FLUTTER_ROOT=/Users/zmt/Documents/ohos/flutter/flutter_flutter
+FLUTTER_APPLICATION_PATH=/Users/zmt/Documents/ohos/github/extended_text_field/example
+COCOAPODS_PARALLEL_CODE_SIGN=true
+FLUTTER_BUILD_DIR=build
+FLUTTER_BUILD_NAME=1.0.0
+FLUTTER_BUILD_NUMBER=1
+FLUTTER_ENGINE=/Users/zmt/Documents/ohos/flutter/engine/src
+LOCAL_ENGINE=ohos_debug_unopt_arm64
+ARCHS=arm64
+DART_OBFUSCATION=false
+TRACK_WIDGET_CREATION=true
+TREE_SHAKE_ICONS=false
+PACKAGE_CONFIG=.dart_tool/package_config.json
diff --git a/example/macos/Flutter/ephemeral/flutter_export_environment.sh b/example/macos/Flutter/ephemeral/flutter_export_environment.sh
new file mode 100755
index 0000000..62a343c
--- /dev/null
+++ b/example/macos/Flutter/ephemeral/flutter_export_environment.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+# This is a generated file; do not edit or check into version control.
+export "FLUTTER_ROOT=/Users/zmt/Documents/ohos/flutter/flutter_flutter"
+export "FLUTTER_APPLICATION_PATH=/Users/zmt/Documents/ohos/github/extended_text_field/example"
+export "COCOAPODS_PARALLEL_CODE_SIGN=true"
+export "FLUTTER_BUILD_DIR=build"
+export "FLUTTER_BUILD_NAME=1.0.0"
+export "FLUTTER_BUILD_NUMBER=1"
+export "FLUTTER_ENGINE=/Users/zmt/Documents/ohos/flutter/engine/src"
+export "LOCAL_ENGINE=ohos_debug_unopt_arm64"
+export "ARCHS=arm64"
+export "DART_OBFUSCATION=false"
+export "TRACK_WIDGET_CREATION=true"
+export "TREE_SHAKE_ICONS=false"
+export "PACKAGE_CONFIG=.dart_tool/package_config.json"
diff --git a/example/macos/Podfile b/example/macos/Podfile
new file mode 100644
index 0000000..dade8df
--- /dev/null
+++ b/example/macos/Podfile
@@ -0,0 +1,40 @@
+platform :osx, '10.11'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock
new file mode 100644
index 0000000..1b2e896
--- /dev/null
+++ b/example/macos/Podfile.lock
@@ -0,0 +1,22 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - url_launcher_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ url_launcher_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
+
+SPEC CHECKSUMS:
+ FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+ url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.10.0
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..cfa0bab
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,632 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 7AF4D8477C1FAF78892E7B47 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F20835456926299CD3CDCE5 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 8A07DB08724BE1ACDF26F964 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ 9F20835456926299CD3CDCE5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D7B7FF68CC01EE21C6512FAA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ FABA45EBEEAD51CFF206571D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 7AF4D8477C1FAF78892E7B47 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ E5F3109C001394DBA2121E0A /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* example.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 9F20835456926299CD3CDCE5 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ E5F3109C001394DBA2121E0A /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ FABA45EBEEAD51CFF206571D /* Pods-Runner.debug.xcconfig */,
+ 8A07DB08724BE1ACDF26F964 /* Pods-Runner.release.xcconfig */,
+ D7B7FF68CC01EE21C6512FAA /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 3EF585B484D5C35B2252DA86 /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ BF4155FDD3B79E6AF9E31812 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 1300;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+ 3EF585B484D5C35B2252DA86 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ BF4155FDD3B79E6AF9E31812 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..fb7259e
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000..d53ef64
--- /dev/null
+++ b/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..a2ec33f
--- /dev/null
+++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000..3c4935a
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000..ed4cc16
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000..483be61
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000..bcbf36d
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000..9c0a652
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000..e71a726
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000..8a31fe2
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000..80e867a
--- /dev/null
+++ b/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,343 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000..8b42559
--- /dev/null
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.example
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved.
diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000..36b0fd9
--- /dev/null
+++ b/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000..dff4f49
--- /dev/null
+++ b/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000..42bcbf4
--- /dev/null
+++ b/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000..dddb8a3
--- /dev/null
+++ b/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist
new file mode 100644
index 0000000..4789daa
--- /dev/null
+++ b/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000..2722837
--- /dev/null
+++ b/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController.init()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index b9174cc..15af0ce 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -14,29 +14,33 @@ publish_to: none
version: 1.0.0+1
environment:
- sdk: ">=2.6.0 <2.12.0"
- flutter: ">=1.22.0"
+ sdk: '>=2.17.0 <3.0.0'
+ flutter: ">=3.7.0"
dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
- cupertino_icons: ^0.1.2
- extended_text: ^6.0.0-non-null-safety
- extended_text_field:
- path: ../
+
+ cupertino_icons: ^1.0.4
+
+ #extended_text: ^9.0.0
+ #extended_text_library: ^9.0.0
+ ff_annotation_route_library: ^3.0.0
+ extended_text: ^10.0.0
flutter:
sdk: flutter
- loading_more_list: ^3.1.1
- oktoast: ^2.1.4
- url_launcher: 5.3.0
-
+ html: ^0.15.0
+ loading_more_list: ^4.1.0
+ oktoast: ^3.1.5
+ url_launcher: ^6.0.17
dev_dependencies:
-
- ff_annotation_route: ^4.0.2
flutter_test:
sdk: flutter
+dependency_overrides:
+ extended_text_field:
+ path: ../
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
deleted file mode 100644
index 747db1d..0000000
--- a/example/test/widget_test.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility that Flutter provides. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:example/main.dart';
-
-void main() {
- testWidgets('Counter increments smoke test', (WidgetTester tester) async {
- // Build our app and trigger a frame.
- await tester.pumpWidget(MyApp());
-
- // Verify that our counter starts at 0.
- expect(find.text('0'), findsOneWidget);
- expect(find.text('1'), findsNothing);
-
- // Tap the '+' icon and trigger a frame.
- await tester.tap(find.byIcon(Icons.add));
- await tester.pump();
-
- // Verify that our counter has incremented.
- expect(find.text('0'), findsNothing);
- expect(find.text('1'), findsOneWidget);
- });
-}
diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ
diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ
diff --git a/example/web/index.html b/example/web/index.html
index 22fd23d..b6b9dd2 100644
--- a/example/web/index.html
+++ b/example/web/index.html
@@ -1,6 +1,21 @@
+
+
+
@@ -22,12 +37,68 @@
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
-
diff --git a/example/web/manifest.json b/example/web/manifest.json
index 8c01291..096edf8 100644
--- a/example/web/manifest.json
+++ b/example/web/manifest.json
@@ -18,6 +18,18 @@
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
}
]
}
diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt
index c7a8c76..744f08a 100644
--- a/example/windows/flutter/CMakeLists.txt
+++ b/example/windows/flutter/CMakeLists.txt
@@ -91,6 +91,7 @@ add_custom_command(
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $
+ VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc
index 4bfa0f3..4f78848 100644
--- a/example/windows/flutter/generated_plugin_registrant.cc
+++ b/example/windows/flutter/generated_plugin_registrant.cc
@@ -2,8 +2,13 @@
// Generated file. Do not edit.
//
+// clang-format off
+
#include "generated_plugin_registrant.h"
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ UrlLauncherWindowsRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/example/windows/flutter/generated_plugin_registrant.h b/example/windows/flutter/generated_plugin_registrant.h
index 9846246..dc139d8 100644
--- a/example/windows/flutter/generated_plugin_registrant.h
+++ b/example/windows/flutter/generated_plugin_registrant.h
@@ -2,6 +2,8 @@
// Generated file. Do not edit.
//
+// clang-format off
+
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake
index 4d10c25..88b22e5 100644
--- a/example/windows/flutter/generated_plugins.cmake
+++ b/example/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,10 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ url_launcher_windows
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -13,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST})
list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/lib/extended_text_field.dart b/lib/extended_text_field.dart
index 70892b8..c9cbe59 100644
--- a/lib/extended_text_field.dart
+++ b/lib/extended_text_field.dart
@@ -2,3 +2,5 @@ library extended_text_field;
export 'package:extended_text_library/extended_text_library.dart';
export 'src/extended_text_field.dart';
+export 'src/keyboard/binding.dart';
+export 'src/keyboard/focus_node.dart';
diff --git a/lib/src/extended_editable_text.dart b/lib/src/extended_editable_text.dart
index 7912eaf..66219f8 100644
--- a/lib/src/extended_editable_text.dart
+++ b/lib/src/extended_editable_text.dart
@@ -2,24 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+// ignore_for_file: unnecessary_null_comparison, always_put_control_body_on_new_line
+
import 'dart:async';
-import 'dart:math';
+import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:extended_text_field/src/extended_render_editable.dart';
import 'package:extended_text_library/extended_text_library.dart';
import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
-import 'package:flutter/painting.dart';
-import 'package:flutter/rendering.dart';
+import 'package:flutter/rendering.dart' hide VerticalCaretMovementRun;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
-import 'package:flutter/gestures.dart' show DragStartBehavior;
-
-/// Signature for the callback that reports when the user changes the selection
-/// (including the cursor location).
-typedef SelectionChangedCallback = void Function(
- TextSelection selection, SelectionChangedCause cause);
// The time it takes for the cursor to fade from fully opaque to fully
// transparent and vice versa. A full cursor blink, from transparent to opaque
@@ -34,6 +30,10 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
+// The minimum width of an iPad screen. The smallest iPad is currently the
+// iPad Mini 6th Gen according to ios-resolution.com.
+const double _kIPadWidth = 1488.0;
+
/// A basic text input field.
///
/// This widget interacts with the [TextInput] service to let the user edit the
@@ -112,21 +112,21 @@ class ExtendedEditableText extends StatefulWidget {
/// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer], and
/// [readOnly] arguments must not be null.
ExtendedEditableText({
- Key key,
+ Key? key,
this.specialTextSpanBuilder,
- @required this.controller,
- @required this.focusNode,
+ required this.controller,
+ required this.focusNode,
this.readOnly = false,
this.obscuringCharacter = '•',
this.obscureText = false,
this.autocorrect = true,
- SmartDashesType smartDashesType,
- SmartQuotesType smartQuotesType,
+ SmartDashesType? smartDashesType,
+ SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
- @required this.style,
- StrutStyle strutStyle,
- @required this.cursorColor,
- @required this.backgroundCursorColor,
+ required this.style,
+ StrutStyle? strutStyle,
+ required this.cursorColor,
+ required this.backgroundCursorColor,
this.textAlign = TextAlign.start,
this.textDirection,
this.locale,
@@ -138,11 +138,11 @@ class ExtendedEditableText extends StatefulWidget {
this.textHeightBehavior,
this.textWidthBasis = TextWidthBasis.parent,
this.autofocus = false,
- bool showCursor,
+ bool? showCursor,
this.showSelectionHandles = false,
this.selectionColor,
this.selectionControls,
- TextInputType keyboardType,
+ TextInputType? keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.onChanged,
@@ -151,7 +151,7 @@ class ExtendedEditableText extends StatefulWidget {
this.onAppPrivateCommand,
this.onSelectionChanged,
this.onSelectionHandleTapped,
- List inputFormatters,
+ List? inputFormatters,
this.mouseCursor,
this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0,
@@ -175,9 +175,14 @@ class ExtendedEditableText extends StatefulWidget {
paste: true,
selectAll: true,
),
- this.autofillHints,
+ this.autofillHints = const [],
+ this.autofillClient,
this.clipBehavior = Clip.hardEdge,
this.restorationId,
+ this.scrollBehavior,
+ this.enableIMEPersonalizedLearning = true,
+ this.showToolbarInWeb = false,
+ this.scribbleEnabled = true,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1),
@@ -219,6 +224,7 @@ class ExtendedEditableText extends StatefulWidget {
assert(dragStartBehavior != null),
assert(toolbarOptions != null),
assert(clipBehavior != null),
+ assert(enableIMEPersonalizedLearning != null),
_strutStyle = strutStyle,
keyboardType = keyboardType ??
_inferKeyboardType(
@@ -233,8 +239,10 @@ class ExtendedEditableText extends StatefulWidget {
showCursor = showCursor ?? !readOnly,
super(key: key);
+ final bool showToolbarInWeb;
+
///build your ccustom text span
- final SpecialTextSpanBuilder specialTextSpanBuilder;
+ final SpecialTextSpanBuilder? specialTextSpanBuilder;
/// Controls the text being edited.
final TextEditingController controller;
@@ -255,14 +263,16 @@ class ExtendedEditableText extends StatefulWidget {
/// Whether to hide the text being edited (e.g., for passwords).
///
/// When this is set to true, all the characters in the text field are
- /// replaced by [obscuringCharacter].
+ /// replaced by [obscuringCharacter], and the text in the field cannot be
+ /// copied with copy or cut. If [readOnly] is also true, then the text cannot
+ /// be selected.
///
/// Defaults to false. Cannot be null.
/// {@endtemplate}
final bool obscureText;
- /// {@macro flutter.dart:ui.textHeightBehavior},
- final TextHeightBehavior textHeightBehavior;
+ /// {@macro dart.ui.textHeightBehavior}
+ final TextHeightBehavior? textHeightBehavior;
/// {@macro flutter.painting.textPainter.textWidthBasis}
final TextWidthBasis textWidthBasis;
@@ -291,8 +301,10 @@ class ExtendedEditableText extends StatefulWidget {
/// Configuration of toolbar options.
///
- /// By default, all options are enabled. If [readOnly] is true,
- /// paste and cut will be disabled regardless.
+ /// By default, all options are enabled. If [readOnly] is true, paste and cut
+ /// will be disabled regardless. If [obscureText] is true, cut and copy will
+ /// be disabled regardless. If [readOnly] and [obscureText] are both true,
+ /// select all will also be disabled.
final ToolbarOptions toolbarOptions;
/// Whether to show selection handles.
@@ -303,7 +315,7 @@ class ExtendedEditableText extends StatefulWidget {
///
/// See also:
///
- /// * [showCursor], which controls the visibility of the cursor..
+ /// * [showCursor], which controls the visibility of the cursor.
final bool showSelectionHandles;
/// {@template flutter.widgets.editableText.showCursor}
@@ -324,13 +336,13 @@ class ExtendedEditableText extends StatefulWidget {
/// {@endtemplate}
final bool autocorrect;
- /// {@macro flutter.services.textInput.smartDashesType}
+ /// {@macro flutter.services.TextInputConfiguration.smartDashesType}
final SmartDashesType smartDashesType;
- /// {@macro flutter.services.textInput.smartQuotesType}
+ /// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
final SmartQuotesType smartQuotesType;
- /// {@macro flutter.services.textInput.enableSuggestions}
+ /// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
final bool enableSuggestions;
/// The text style to use for the editable text.
@@ -361,18 +373,13 @@ class ExtendedEditableText extends StatefulWidget {
/// default values, and will instead inherit omitted/null properties from the
/// [TextStyle] instead. See [StrutStyle.inheritFromTextStyle].
StrutStyle get strutStyle {
- return _strutStyle;
-
- ///not good for widgetSpan
- // if (_strutStyle == null) {
- // return style != null
- // ? StrutStyle.fromTextStyle(style, forceStrutHeight: true)
- // : const StrutStyle();
- // }
- // return _strutStyle.inheritFromTextStyle(style);
+ if (_strutStyle == null) {
+ return StrutStyle.fromTextStyle(style, forceStrutHeight: true);
+ }
+ return _strutStyle!.inheritFromTextStyle(style);
}
- final StrutStyle _strutStyle;
+ final StrutStyle? _strutStyle;
/// {@template flutter.widgets.editableText.textAlign}
/// How the text should be aligned horizontally.
@@ -394,18 +401,9 @@ class ExtendedEditableText extends StatefulWidget {
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
- /// When LTR text is entered into an RTL field, or RTL text is entered into an
- /// LTR field, [LRM](https://en.wikipedia.org/wiki/Left-to-right_mark) or
- /// [RLM](https://en.wikipedia.org/wiki/Right-to-left_mark) characters will be
- /// inserted alongside whitespace characters, respectively. This is to
- /// eliminate ambiguous directionality in whitespace and ensure proper caret
- /// placement. These characters will affect the length of the string and may
- /// need to be parsed out when doing things like string comparison with other
- /// text.
- ///
/// Defaults to the ambient [Directionality], if any.
/// {@endtemplate}
- final TextDirection textDirection;
+ final TextDirection? textDirection;
/// {@template flutter.widgets.editableText.textCapitalization}
/// Configures how the platform keyboard will select an uppercase or
@@ -430,7 +428,7 @@ class ExtendedEditableText extends StatefulWidget {
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
///
/// See [RenderEditable.locale] for more information.
- final Locale locale;
+ final Locale? locale;
/// {@template flutter.widgets.editableText.textScaleFactor}
/// The number of font pixels for each logical pixel.
@@ -441,7 +439,7 @@ class ExtendedEditableText extends StatefulWidget {
/// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
/// {@endtemplate}
- final double textScaleFactor;
+ final double? textScaleFactor;
/// The color to use when painting the cursor.
///
@@ -458,7 +456,7 @@ class ExtendedEditableText extends StatefulWidget {
/// Currently the autocorrection Rect only appears on iOS.
///
/// Defaults to null, which disables autocorrection Rect painting.
- final Color autocorrectionTextRectColor;
+ final Color? autocorrectionTextRectColor;
/// The color to use when painting the background cursor aligned with the text
/// while rendering the floating cursor.
@@ -468,23 +466,27 @@ class ExtendedEditableText extends StatefulWidget {
final Color backgroundCursorColor;
/// {@template flutter.widgets.editableText.maxLines}
- /// The maximum number of lines for the text to span, wrapping if necessary.
+ /// The maximum number of lines to show at one time, wrapping if necessary.
+ ///
+ /// This affects the height of the field itself and does not limit the number
+ /// of lines that can be entered into the field.
///
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
///
/// If this is null, there is no limit to the number of lines, and the text
/// container will start with enough vertical space for one line and
- /// automatically grow to accommodate additional lines as they are entered.
+ /// automatically grow to accommodate additional lines as they are entered, up
+ /// to the height of its constraints.
///
/// If this is not null, the value must be greater than zero, and it will lock
/// the input to the given number of lines and take up enough horizontal space
/// to accommodate that number of lines. Setting [minLines] as well allows the
- /// input to grow between the indicated range.
+ /// input to grow and shrink between the indicated range.
///
/// The full set of behaviors possible with [minLines] and [maxLines] are as
- /// follows. These examples apply equally to `TextField`, `TextFormField`, and
- /// `EditableText`.
+ /// follows. These examples apply equally to [TextField], [TextFormField],
+ /// [CupertinoTextField], and [EditableText].
///
/// Input that occupies a single line and scrolls horizontally as needed.
/// ```dart
@@ -509,12 +511,21 @@ class ExtendedEditableText extends StatefulWidget {
/// ```dart
/// TextField(minLines: 2, maxLines: 4)
/// ```
+ ///
+ /// See also:
+ ///
+ /// * [minLines], which sets the minimum number of lines visible.
/// {@endtemplate}
- final int maxLines;
+ /// * [expands], which determines whether the field should fill the height of
+ /// its parent.
+ final int? maxLines;
/// {@template flutter.widgets.editableText.minLines}
/// The minimum number of lines to occupy when the content spans fewer lines.
///
+ /// This affects the height of the field itself and does not limit the number
+ /// of lines that can be entered into the field.
+ ///
/// If this is null (default), text container starts with enough vertical space
/// for one line and grows to accommodate additional lines as they are entered.
///
@@ -529,8 +540,8 @@ class ExtendedEditableText extends StatefulWidget {
/// starting from [minLines].
///
/// A few examples of behaviors possible with [minLines] and [maxLines] are as follows.
- /// These apply equally to `TextField`, `TextFormField`, `CupertinoTextField`,
- /// and `EditableText`.
+ /// These apply equally to [TextField], [TextFormField], [CupertinoTextField],
+ /// and [EditableText].
///
/// Input that always occupies at least 2 lines and has an infinite max.
/// Expands vertically as needed.
@@ -545,12 +556,17 @@ class ExtendedEditableText extends StatefulWidget {
/// TextField(minLines:2, maxLines: 4)
/// ```
///
- /// See the examples in [maxLines] for the complete picture of how [maxLines]
- /// and [minLines] interact to produce various behaviors.
- ///
/// Defaults to null.
+ ///
+ /// See also:
+ ///
+ /// * [maxLines], which sets the maximum number of lines visible, and has
+ /// several examples of how minLines and maxLines interact to produce
+ /// various behaviors.
/// {@endtemplate}
- final int minLines;
+ /// * [expands], which determines whether the field should fill the height of
+ /// its parent.
+ final int? minLines;
/// {@template flutter.widgets.editableText.expands}
/// Whether this widget's height will be sized to fill its parent.
@@ -590,26 +606,31 @@ class ExtendedEditableText extends StatefulWidget {
/// The color to use when painting the selection.
///
+ /// If this property is null, this widget gets the selection color from the
+ /// [DefaultSelectionStyle].
+ ///
/// For [CupertinoTextField]s, the value is set to the ambient
/// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
- /// value is set to the ambient [ThemeData.textSelectionColor].
- final Color selectionColor;
+ /// value is set to the ambient [TextSelectionThemeData.selectionColor].
+ final Color? selectionColor;
+ /// {@template flutter.widgets.editableText.selectionControls}
/// Optional delegate for building the text selection handles and toolbar.
///
- /// The [ExtendedEditableText] widget used on its own will not trigger the display
+ /// The [EditableText] widget used on its own will not trigger the display
/// of the selection toolbar by itself. The toolbar is shown by calling
- /// [ExtendedEditableTextState.showToolbar] in response to an appropriate user event.
+ /// [EditableTextState.showToolbar] in response to an appropriate user event.
///
/// See also:
///
- /// * [CupertinoTextField], which wraps an [ExtendedEditableText] and which shows the
+ /// * [CupertinoTextField], which wraps an [EditableText] and which shows the
/// selection toolbar upon user events that are appropriate on the iOS
/// platform.
- /// * [TextField], a Material Design themed wrapper of [ExtendedEditableText], which
+ /// * [TextField], a Material Design themed wrapper of [EditableText], which
/// shows the selection toolbar upon appropriate user events based on the
/// user's platform set in [ThemeData.platform].
- final TextSelectionControls selectionControls;
+ /// {@endtemplate}
+ final TextSelectionControls? selectionControls;
/// {@template flutter.widgets.editableText.keyboardType}
/// The type of keyboard to use for editing the text.
@@ -620,7 +641,7 @@ class ExtendedEditableText extends StatefulWidget {
final TextInputType keyboardType;
/// The type of action button to use with the soft keyboard.
- final TextInputAction textInputAction;
+ final TextInputAction? textInputAction;
/// {@template flutter.widgets.editableText.onChanged}
/// Called when the user initiates a change to the TextField's
@@ -635,69 +656,41 @@ class ExtendedEditableText extends StatefulWidget {
/// and selection, one can add a listener to its [controller] with
/// [TextEditingController.addListener].
///
- /// {@tool dartpad --template=stateful_widget_material}
+ /// [onChanged] is called before [onSubmitted] when user indicates completion
+ /// of editing, such as when pressing the "done" button on the keyboard. That default
+ /// behavior can be overridden. See [onEditingComplete] for details.
///
+ /// {@tool dartpad}
/// This example shows how onChanged could be used to check the TextField's
/// current value each time the user inserts or deletes a character.
///
- /// ```dart
- /// TextEditingController _controller;
- ///
- /// void initState() {
- /// super.initState();
- /// _controller = TextEditingController();
- /// }
- ///
- /// void dispose() {
- /// _controller.dispose();
- /// super.dispose();
- /// }
- ///
- /// Widget build(BuildContext context) {
- /// return Scaffold(
- /// body: Column(
- /// mainAxisAlignment: MainAxisAlignment.center,
- /// children: [
- /// const Text('What number comes next in the sequence?'),
- /// const Text('1, 1, 2, 3, 5, 8...?'),
- /// TextField(
- /// controller: _controller,
- /// onChanged: (String value) async {
- /// if (value != '13') {
- /// return;
- /// }
- /// await showDialog(
- /// context: context,
- /// builder: (BuildContext context) {
- /// return AlertDialog(
- /// title: const Text('Thats correct!'),
- /// content: Text ('13 is the right answer.'),
- /// actions: [
- /// TextButton(
- /// onPressed: () { Navigator.pop(context); },
- /// child: const Text('OK'),
- /// ),
- /// ],
- /// );
- /// },
- /// );
- /// },
- /// ),
- /// ],
- /// ),
- /// );
- /// }
- /// ```
+ /// ** See code in examples/api/lib/widgets/editable_text/editable_text.on_changed.0.dart **
/// {@end-tool}
/// {@endtemplate}
///
+ /// ## Handling emojis and other complex characters
+ /// {@template flutter.widgets.EditableText.onChanged}
+ /// It's important to always use
+ /// [characters](https://pub.dev/packages/characters) when dealing with user
+ /// input text that may contain complex characters. This will ensure that
+ /// extended grapheme clusters and surrogate pairs are treated as single
+ /// characters, as they appear to the user.
+ ///
+ /// For example, when finding the length of some user input, use
+ /// `string.characters.length`. Do NOT use `string.length` or even
+ /// `string.runes.length`. For the complex character "���", this
+ /// appears to the user as a single character, and `string.characters.length`
+ /// intuitively returns 1. On the other hand, `string.length` returns 8, and
+ /// `string.runes.length` returns 5!
+ /// {@endtemplate}
+ ///
/// See also:
///
/// * [inputFormatters], which are called before [onChanged]
/// runs and can validate and change ("format") the input value.
/// * [onEditingComplete], [onSubmitted], [onSelectionChanged]:
/// which are more specialized input change notifications.
- final ValueChanged onChanged;
+ final ValueChanged? onChanged;
/// {@template flutter.widgets.editableText.onEditingComplete}
/// Called when the user submits editable content (e.g., user presses the "done"
@@ -717,89 +710,54 @@ class ExtendedEditableText extends StatefulWidget {
///
/// Providing [onEditingComplete] prevents the aforementioned default behavior.
/// {@endtemplate}
- final VoidCallback onEditingComplete;
+ final VoidCallback? onEditingComplete;
/// {@template flutter.widgets.editableText.onSubmitted}
/// Called when the user indicates that they are done editing the text in the
/// field.
- /// {@endtemplate}
///
- /// {@tool dartpad --template=stateful_widget_material}
- /// When a non-completion action is pressed, such as "next" or "previous", it
- /// is often desirable to move the focus to the next or previous field. To do
- /// this, handle it as in this example, by calling [FocusNode.nextFocus] in
- /// the `onFieldSubmitted` callback of [TextFormField]. ([TextFormField] wraps
- /// [EditableText] internally, and uses the value of `onFieldSubmitted` as its
- /// [onSubmitted]).
- ///
- /// ```dart
- /// FocusScopeNode _focusScopeNode = FocusScopeNode();
- /// final _controller1 = TextEditingController();
- /// final _controller2 = TextEditingController();
- ///
- /// void dispose() {
- /// _focusScopeNode.dispose();
- /// _controller1.dispose();
- /// _controller2.dispose();
- /// super.dispose();
- /// }
- ///
- /// void _handleSubmitted(String value) {
- /// _focusScopeNode.nextFocus();
- /// }
- ///
- /// Widget build(BuildContext context) {
- /// return Scaffold(
- /// body: FocusScope(
- /// node: _focusScopeNode,
- /// child: Column(
- /// mainAxisAlignment: MainAxisAlignment.center,
- /// children: [
- /// Padding(
- /// padding: const EdgeInsets.all(8.0),
- /// child: TextFormField(
- /// textInputAction: TextInputAction.next,
- /// onFieldSubmitted: _handleSubmitted,
- /// controller: _controller1,
- /// decoration: InputDecoration(border: OutlineInputBorder()),
- /// ),
- /// ),
- /// Padding(
- /// padding: const EdgeInsets.all(8.0),
- /// child: TextFormField(
- /// textInputAction: TextInputAction.next,
- /// onFieldSubmitted: _handleSubmitted,
- /// controller: _controller2,
- /// decoration: InputDecoration(border: OutlineInputBorder()),
- /// ),
- /// ),
- /// ],
- /// ),
- /// ),
- /// );
- /// }
- /// ```
- /// {@end-tool}
- final ValueChanged onSubmitted;
+ /// By default, [onSubmitted] is called after [onChanged] when the user
+ /// has finalized editing; or, if the default behavior has been overridden,
+ /// after [onEditingComplete]. See [onEditingComplete] for details.
+ /// {@endtemplate}
+ final ValueChanged? onSubmitted;
/// {@template flutter.widgets.editableText.onAppPrivateCommand}
- /// Called when the result of an app private command is received.
+ /// This is used to receive a private command from the input method.
+ ///
+ /// Called when the result of [TextInputClient.performPrivateCommand] is
+ /// received.
+ ///
+ /// This can be used to provide domain-specific features that are only known
+ /// between certain input methods and their clients.
+ ///
+ /// See also:
+ /// * [performPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputConnection#performPrivateCommand\(java.lang.String,%20android.os.Bundle\)),
+ /// which is the Android documentation for performPrivateCommand, used to
+ /// send a command from the input method.
+ /// * [sendAppPrivateCommand](https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#sendAppPrivateCommand),
+ /// which is the Android documentation for sendAppPrivateCommand, used to
+ /// send a command to the input method.
/// {@endtemplate}
- final AppPrivateCommandCallback onAppPrivateCommand;
+ final AppPrivateCommandCallback? onAppPrivateCommand;
+ /// {@template flutter.widgets.editableText.onSelectionChanged}
/// Called when the user changes the selection of text (including the cursor
/// location).
- final SelectionChangedCallback onSelectionChanged;
+ /// {@endtemplate}
+ final SelectionChangedCallback? onSelectionChanged;
- /// {@macro flutter.widgets.textSelection.onSelectionHandleTapped}
- final VoidCallback onSelectionHandleTapped;
+ /// {@macro flutter.widgets.TextSelectionOverlay.onSelectionHandleTapped}
+ final VoidCallback? onSelectionHandleTapped;
/// {@template flutter.widgets.editableText.inputFormatters}
/// Optional input validation and formatting overrides.
///
- /// Formatters are run in the provided order when the text input changes.
+ /// Formatters are run in the provided order when the text input changes. When
+ /// this parameter changes, the new formatters will not be applied until the
+ /// next time the user inserts or deletes text.
/// {@endtemplate}
- final List inputFormatters;
+ final List? inputFormatters;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
@@ -810,7 +768,7 @@ class ExtendedEditableText extends StatefulWidget {
/// appearance of the mouse pointer. All other properties related to "cursor"
/// stands for the text cursor, which is usually a blinking vertical line at
/// the editing position.
- final MouseCursor mouseCursor;
+ final MouseCursor? mouseCursor;
/// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [RenderEditable] and [RenderEditable.ignorePointer].
@@ -821,7 +779,7 @@ class ExtendedEditableText extends StatefulWidget {
/// {@template flutter.widgets.editableText.cursorWidth}
/// How thick the cursor will be.
///
- /// Defaults to 2.0
+ /// Defaults to 2.0.
///
/// The cursor will draw under the text. The cursor width will extend
/// to the right of the boundary between characters for left-to-right text
@@ -836,14 +794,14 @@ class ExtendedEditableText extends StatefulWidget {
///
/// If this property is null, [RenderEditable.preferredLineHeight] will be used.
/// {@endtemplate}
- final double cursorHeight;
+ final double? cursorHeight;
/// {@template flutter.widgets.editableText.cursorRadius}
/// How rounded the corners of the cursor should be.
///
/// By default, the cursor has no radius.
/// {@endtemplate}
- final Radius cursorRadius;
+ final Radius? cursorRadius;
/// Whether the cursor will animate from fully transparent to fully opaque
/// during each cursor blink.
@@ -852,10 +810,10 @@ class ExtendedEditableText extends StatefulWidget {
/// animate on Android platforms.
final bool cursorOpacityAnimates;
- ///{@macro flutter.rendering.editable.cursorOffset}
- final Offset cursorOffset;
+ ///{@macro flutter.rendering.RenderEditable.cursorOffset}
+ final Offset? cursorOffset;
- ///{@macro flutter.rendering.editable.paintCursorOnTop}
+ ///{@macro flutter.rendering.RenderEditable.paintCursorAboveText}
final bool paintCursorAboveText;
/// Controls how tall the selection highlight boxes are computed to be.
@@ -898,6 +856,8 @@ class ExtendedEditableText extends StatefulWidget {
/// When this is false, the text selection cannot be adjusted by
/// the user, text cannot be copied, and the user cannot paste into
/// the text field from the clipboard.
+ ///
+ /// Defaults to true.
/// {@endtemplate}
final bool enableInteractiveSelection;
@@ -921,7 +881,7 @@ class ExtendedEditableText extends StatefulWidget {
///
/// See [Scrollable.controller].
/// {@endtemplate}
- final ScrollController scrollController;
+ final ScrollController? scrollController;
/// {@template flutter.widgets.editableText.scrollPhysics}
/// The [ScrollPhysics] to use when vertically scrolling the input.
@@ -930,7 +890,20 @@ class ExtendedEditableText extends StatefulWidget {
///
/// See [Scrollable.physics].
/// {@endtemplate}
- final ScrollPhysics scrollPhysics;
+ ///
+ /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the
+ /// [ScrollPhysics] provided by that behavior will take precedence after
+ /// [scrollPhysics].
+ final ScrollPhysics? scrollPhysics;
+
+ /// {@template flutter.widgets.editableText.scribbleEnabled}
+ /// Whether iOS 14 Scribble features are enabled for this widget.
+ ///
+ /// Only available on iPads.
+ ///
+ /// Defaults to true.
+ /// {@endtemplate}
+ final bool scribbleEnabled;
/// {@template flutter.widgets.editableText.selectionEnabled}
/// Same as [enableInteractiveSelection].
@@ -944,16 +917,18 @@ class ExtendedEditableText extends StatefulWidget {
/// A list of strings that helps the autofill service identify the type of this
/// text input.
///
- /// When set to null or empty, the text input will not send any autofill related
- /// information to the platform. As a result, it will not participate in
- /// autofills triggered by a different [AutofillClient], even if they're in the
- /// same [AutofillScope]. Additionally, on Android and web, setting this to null
- /// or empty will disable autofill for this text field.
+ /// When set to null, this text input will not send its autofill information
+ /// to the platform, preventing it from participating in autofills triggered
+ /// by a different [AutofillClient], even if they're in the same
+ /// [AutofillScope]. Additionally, on Android and web, setting this to null
+ /// will disable autofill for this text field.
///
/// The minimum platform SDK version that supports Autofill is API level 26
/// for Android, and iOS 10.0 for iOS.
///
- /// ### iOS-specific Concerns:
+ /// Defaults to an empty list.
+ ///
+ /// ### Setting up iOS autofill:
///
/// To provide the best user experience and ensure your app fully supports
/// password autofill on iOS, follow these steps:
@@ -965,11 +940,58 @@ class ExtendedEditableText extends StatefulWidget {
/// works only with [TextInputType.emailAddress]. Make sure the input field has a
/// compatible [keyboardType]. Empirically, [TextInputType.name] works well
/// with many autofill hints that are predefined on iOS.
+ ///
+ /// ### Troubleshooting Autofill
+ ///
+ /// Autofill service providers rely heavily on [autofillHints]. Make sure the
+ /// entries in [autofillHints] are supported by the autofill service currently
+ /// in use (the name of the service can typically be found in your mobile
+ /// device's system settings).
+ ///
+ /// #### Autofill UI refuses to show up when I tap on the text field
+ ///
+ /// Check the device's system settings and make sure autofill is turned on,
+ /// and there are available credentials stored in the autofill service.
+ ///
+ /// * iOS password autofill: Go to Settings -> Password, turn on "Autofill
+ /// Passwords", and add new passwords for testing by pressing the top right
+ /// "+" button. Use an arbitrary "website" if you don't have associated
+ /// domains set up for your app. As long as there's at least one password
+ /// stored, you should be able to see a key-shaped icon in the quick type
+ /// bar on the software keyboard, when a password related field is focused.
+ ///
+ /// * iOS contact information autofill: iOS seems to pull contact info from
+ /// the Apple ID currently associated with the device. Go to Settings ->
+ /// Apple ID (usually the first entry, or "Sign in to your iPhone" if you
+ /// haven't set up one on the device), and fill out the relevant fields. If
+ /// you wish to test more contact info types, try adding them in Contacts ->
+ /// My Card.
+ ///
+ /// * Android autofill: Go to Settings -> System -> Languages & input ->
+ /// Autofill service. Enable the autofill service of your choice, and make
+ /// sure there are available credentials associated with your app.
+ ///
+ /// #### I called `TextInput.finishAutofillContext` but the autofill save
+ /// prompt isn't showing
+ ///
+ /// * iOS: iOS may not show a prompt or any other visual indication when it
+ /// saves user password. Go to Settings -> Password and check if your new
+ /// password is saved. Neither saving password nor auto-generating strong
+ /// password works without properly setting up associated domains in your
+ /// app. To set up associated domains, follow the instructions in
+ /// .
+ ///
/// {@endtemplate}
- /// {@macro flutter.services.autofill.autofillHints}
- final Iterable autofillHints;
+ /// {@macro flutter.services.AutofillConfiguration.autofillHints}
+ final Iterable? autofillHints;
- /// {@macro flutter.widgets.Clip}
+ /// The [AutofillClient] that controls this input field's autofill behavior.
+ ///
+ /// When null, this widget's [EditableTextState] will be used as the
+ /// [AutofillClient]. This property may override [autofillHints].
+ final AutofillClient? autofillClient;
+
+ /// {@macro flutter.material.Material.clipBehavior}
///
/// Defaults to [Clip.hardEdge].
final Clip clipBehavior;
@@ -991,17 +1013,37 @@ class ExtendedEditableText extends StatefulWidget {
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
- final String restorationId;
+ final String? restorationId;
+
+ /// {@template flutter.widgets.shadow.scrollBehavior}
+ /// A [ScrollBehavior] that will be applied to this widget individually.
+ ///
+ /// Defaults to null, wherein the inherited [ScrollBehavior] is copied and
+ /// modified to alter the viewport decoration, like [Scrollbar]s.
+ /// {@endtemplate}
+ ///
+ /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
+ /// [ScrollPhysics] is provided in [scrollPhysics], it will take precedence,
+ /// followed by [scrollBehavior], and then the inherited ancestor
+ /// [ScrollBehavior].
+ ///
+ /// The [ScrollBehavior] of the inherited [ScrollConfiguration] will be
+ /// modified by default to only apply a [Scrollbar] if [maxLines] is greater
+ /// than 1.
+ final ScrollBehavior? scrollBehavior;
+
+ /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
+ final bool enableIMEPersonalizedLearning;
+
// Infer the keyboard type of an `EditableText` if it's not specified.
static TextInputType _inferKeyboardType({
- @required Iterable autofillHints,
- @required int maxLines,
+ required Iterable? autofillHints,
+ required int? maxLines,
}) {
- if (autofillHints?.isEmpty ?? true) {
+ if (autofillHints == null || autofillHints.isEmpty) {
return maxLines == 1 ? TextInputType.text : TextInputType.multiline;
}
- TextInputType returnValue;
final String effectiveHint = autofillHints.first;
// On iOS oftentimes specifying a text content type is not enough to qualify
@@ -1056,7 +1098,10 @@ class ExtendedEditableText extends StatefulWidget {
AutofillHints.username: TextInputType.text,
};
- returnValue = iOSKeyboardType[effectiveHint];
+ final TextInputType? keyboardType = iOSKeyboardType[effectiveHint];
+ if (keyboardType != null) {
+ return keyboardType;
+ }
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
@@ -1066,8 +1111,9 @@ class ExtendedEditableText extends StatefulWidget {
}
}
- if (returnValue != null || maxLines != 1)
- return returnValue ?? TextInputType.multiline;
+ if (maxLines != 1) {
+ return TextInputType.multiline;
+ }
const Map inferKeyboardType =
{
@@ -1167,7 +1213,7 @@ class ExtendedEditableText extends StatefulWidget {
properties.add(DiagnosticsProperty(
'enableSuggestions', enableSuggestions,
defaultValue: true));
- style?.debugFillProperties(properties);
+ style.debugFillProperties(properties);
properties.add(
EnumProperty('textAlign', textAlign, defaultValue: null));
properties.add(EnumProperty('textDirection', textDirection,
@@ -1197,6 +1243,9 @@ class ExtendedEditableText extends StatefulWidget {
properties.add(DiagnosticsProperty(
'textHeightBehavior', textHeightBehavior,
defaultValue: null));
+ properties.add(DiagnosticsProperty(
+ 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
+ defaultValue: true));
}
}
@@ -1205,30 +1254,40 @@ class ExtendedEditableTextState extends State
with
AutomaticKeepAliveClientMixin,
WidgetsBindingObserver,
- TickerProviderStateMixin
- implements TextInputClient, TextSelectionDelegate, AutofillClient {
- Timer _cursorTimer;
+ TickerProviderStateMixin,
+ // TextEditingActionTarget,
+ TextSelectionDelegate
+ implements
+ TextInputClient,
+ AutofillClient {
+ Timer? _cursorTimer;
bool _targetCursorVisibility = false;
final ValueNotifier _cursorVisibilityNotifier =
ValueNotifier(true);
final GlobalKey _editableKey = GlobalKey();
- final ClipboardStatusNotifier _clipboardStatus =
- kIsWeb ? null : ClipboardStatusNotifier();
+ ClipboardStatusNotifier? _clipboardStatus;
+
+ TextInputConnection? _textInputConnection;
+ ExtendedTextSelectionOverlay? _selectionOverlay;
- TextInputConnection _textInputConnection;
- ExtendedTextSelectionOverlay _selectionOverlay;
- ScrollController _scrollController;
- AnimationController _cursorBlinkOpacityController;
+ ScrollController? _internalScrollController;
+ ScrollController get _scrollController =>
+ widget.scrollController ??
+ (_internalScrollController ??= ScrollController());
+
+ AnimationController? _cursorBlinkOpacityController;
final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink();
final LayerLink _endHandleLayerLink = LayerLink();
+
bool _didAutoFocus = false;
- FocusAttachment _focusAttachment;
- AutofillGroupState _currentAutofillScope;
+ AutofillGroupState? _currentAutofillScope;
@override
- AutofillScope get currentAutofillScope => _currentAutofillScope;
+ AutofillScope? get currentAutofillScope => _currentAutofillScope;
+
+ AutofillClient get _effectiveAutofillClient => widget.autofillClient ?? this;
///whether to support build SpecialText
bool get supportSpecialText =>
@@ -1236,9 +1295,6 @@ class ExtendedEditableTextState extends State
!widget.obscureText &&
_textDirection == TextDirection.ltr;
- // Is this field in the current autofill context.
- bool _isInAutofillContext = false;
-
/// Whether to create an input connection with the platform for text editing
/// or not.
///
@@ -1262,13 +1318,13 @@ class ExtendedEditableTextState extends State
// cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
- AnimationController _floatingCursorResetController;
+ AnimationController? _floatingCursorResetController;
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
Color get _cursorColor =>
- widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
+ widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
@override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly;
@@ -1286,45 +1342,164 @@ class ExtendedEditableTextState extends State
// Inform the widget that the value of clipboardStatus has changed.
});
}
+
+ TextEditingValue get _textEditingValueforTextLayoutMetrics {
+ final Widget? editableWidget = _editableKey.currentContext?.widget;
+ if (editableWidget is! _Editable) {
+ throw StateError('_Editable must be mounted.');
+ }
+ return editableWidget.value;
+ }
+
+ /// Copy current selection to [Clipboard].
+ @override
+ void copySelection(SelectionChangedCause cause) {
+ final TextSelection selection = textEditingValue.selection;
+ final String text = textEditingValue.text;
+ assert(selection != null);
+ if (selection.isCollapsed) {
+ return;
+ }
+ Clipboard.setData(ClipboardData(text: selection.textInside(text)));
+ if (cause == SelectionChangedCause.toolbar) {
+ bringIntoView(textEditingValue.selection.extent);
+ hideToolbar(false);
+
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.iOS:
+ break;
+ case TargetPlatform.macOS:
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ // Collapse the selection and hide the toolbar and handles.
+ userUpdateTextEditingValue(
+ TextEditingValue(
+ text: textEditingValue.text,
+ selection: TextSelection.collapsed(
+ offset: textEditingValue.selection.end),
+ ),
+ SelectionChangedCause.toolbar,
+ );
+ break;
+ }
+ }
+ }
+
+ /// Cut current selection to [Clipboard].
+ @override
+ void cutSelection(SelectionChangedCause cause) {
+ if (widget.readOnly) {
+ return;
+ }
+ final TextSelection selection = textEditingValue.selection;
+ final String text = textEditingValue.text;
+ assert(selection != null);
+ if (selection.isCollapsed) {
+ return;
+ }
+ Clipboard.setData(ClipboardData(text: selection.textInside(text)));
+ _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause));
+ if (cause == SelectionChangedCause.toolbar) {
+ bringIntoView(textEditingValue.selection.extent);
+ hideToolbar();
+ }
+ }
+
+ /// Paste text from [Clipboard].
+ @override
+ Future pasteText(SelectionChangedCause cause) async {
+ if (widget.readOnly) {
+ return;
+ }
+ final TextSelection selection = textEditingValue.selection;
+ assert(selection != null);
+ if (!selection.isValid) {
+ return;
+ }
+ // Snapshot the input before using `await`.
+ // See https://github.com/flutter/flutter/issues/11427
+ final ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
+ if (data == null) {
+ return;
+ }
+
+ _replaceText(
+ ReplaceTextIntent(textEditingValue, data.text!, selection, cause));
+ if (cause == SelectionChangedCause.toolbar) {
+ bringIntoView(textEditingValue.selection.extent);
+ hideToolbar();
+ }
+ }
+
+ /// Select the entire text value.
+ @override
+ void selectAll(SelectionChangedCause cause) {
+ userUpdateTextEditingValue(
+ textEditingValue.copyWith(
+ selection: TextSelection(
+ baseOffset: 0, extentOffset: textEditingValue.text.length),
+ ),
+ cause,
+ );
+ if (cause == SelectionChangedCause.toolbar) {
+ bringIntoView(textEditingValue.selection.extent);
+ }
+ }
+
// State lifecycle:
@override
void initState() {
super.initState();
+ _cursorBlinkOpacityController = AnimationController(
+ vsync: this,
+ duration: _fadeDuration,
+ )..addListener(_onCursorColorTick);
+ _clipboardStatus =
+ kIsWeb && !widget.showToolbarInWeb ? null : ClipboardStatusNotifier();
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue);
- _focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged);
- _scrollController = widget.scrollController ?? ScrollController();
- _scrollController.addListener(() {
- _selectionOverlay?.updateForScroll();
- });
- _cursorBlinkOpacityController =
- AnimationController(vsync: this, duration: _fadeDuration);
- _cursorBlinkOpacityController.addListener(_onCursorColorTick);
- _floatingCursorResetController = AnimationController(vsync: this);
- _floatingCursorResetController.addListener(_onFloatingCursorResetTick);
+ _scrollController.addListener(_updateSelectionOverlayForScroll);
_cursorVisibilityNotifier.value = widget.showCursor;
}
+ // Whether `TickerMode.of(context)` is true and animations (like blinking the
+ // cursor) are supposed to run.
+ bool _tickersEnabled = true;
+
@override
void didChangeDependencies() {
super.didChangeDependencies();
- final AutofillGroupState newAutofillGroup = AutofillGroup.of(context);
+ final AutofillGroupState? newAutofillGroup = AutofillGroup.maybeOf(context);
if (currentAutofillScope != newAutofillGroup) {
_currentAutofillScope?.unregister(autofillId);
_currentAutofillScope = newAutofillGroup;
- newAutofillGroup?.register(this);
- _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
+ _currentAutofillScope?.register(_effectiveAutofillClient);
}
if (!_didAutoFocus && widget.autofocus) {
_didAutoFocus = true;
SchedulerBinding.instance.addPostFrameCallback((_) {
- if (mounted) {
+ if (mounted && renderEditable.hasSize) {
FocusScope.of(context).autofocus(widget.focusNode);
}
});
}
+
+ // Restart or stop the blinking cursor when TickerMode changes.
+ final bool newTickerEnabled = TickerMode.of(context);
+ if (_tickersEnabled != newTickerEnabled) {
+ _tickersEnabled = newTickerEnabled;
+ if (_tickersEnabled && _cursorActive) {
+ _startCursorTimer();
+ } else if (!_tickersEnabled && _cursorTimer != null) {
+ // Cannot use _stopCursorTimer because it would reset _cursorActive.
+ _cursorTimer!.cancel();
+ _cursorTimer = null;
+ }
+ }
}
@override
@@ -1339,27 +1514,43 @@ class ExtendedEditableTextState extends State
_selectionOverlay?.update(_value);
}
_selectionOverlay?.handlesVisible = widget.showSelectionHandles;
- _isInAutofillContext = _isInAutofillContext || _shouldBeInAutofillContext;
+
+ if (widget.autofillClient != oldWidget.autofillClient) {
+ _currentAutofillScope
+ ?.unregister(oldWidget.autofillClient?.autofillId ?? autofillId);
+ _currentAutofillScope?.register(_effectiveAutofillClient);
+ }
+
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged);
- _focusAttachment?.detach();
- _focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
}
+
+ if (widget.scrollController != oldWidget.scrollController) {
+ (oldWidget.scrollController ?? _internalScrollController)
+ ?.removeListener(_updateSelectionOverlayForScroll);
+ _scrollController.addListener(_updateSelectionOverlayForScroll);
+ }
+
if (!_shouldCreateInputConnection) {
_closeInputConnectionIfNeeded();
- } else {
- if (oldWidget.readOnly && _hasFocus) {
- _openInputConnection();
+ } else if (oldWidget.readOnly && _hasFocus) {
+ _openInputConnection();
+ }
+
+ if (kIsWeb && _hasInputConnection) {
+ if (oldWidget.readOnly != widget.readOnly) {
+ _textInputConnection!
+ .updateConfig(_effectiveAutofillClient.textInputConfiguration);
}
}
if (widget.style != oldWidget.style) {
final TextStyle style = widget.style;
// The _textInputConnection will pick up the new style when it attaches in
// _openInputConnection.
- if (_textInputConnection != null && _textInputConnection.attached) {
- _textInputConnection.setStyle(
+ if (_hasInputConnection) {
+ _textInputConnection!.setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
@@ -1373,52 +1564,58 @@ class ExtendedEditableTextState extends State
widget.selectionControls?.canPaste(this) == true) {
_clipboardStatus?.update();
}
+
+ if (oldWidget.showToolbarInWeb != widget.showToolbarInWeb) {
+ _clipboardStatus =
+ kIsWeb && !widget.showToolbarInWeb ? null : ClipboardStatusNotifier();
+ }
}
@override
void dispose() {
+ _internalScrollController?.dispose();
_currentAutofillScope?.unregister(autofillId);
widget.controller.removeListener(_didChangeTextEditingValue);
- _cursorBlinkOpacityController.removeListener(_onCursorColorTick);
- _floatingCursorResetController.removeListener(_onFloatingCursorResetTick);
+ _floatingCursorResetController?.dispose();
+ _floatingCursorResetController = null;
_closeInputConnectionIfNeeded();
assert(!_hasInputConnection);
- _stopCursorTimer();
- assert(_cursorTimer == null);
+ _cursorTimer?.cancel();
+ _cursorTimer = null;
+ _cursorBlinkOpacityController?.dispose();
+ _cursorBlinkOpacityController = null;
_selectionOverlay?.dispose();
_selectionOverlay = null;
- _focusAttachment.detach();
widget.focusNode.removeListener(_handleFocusChanged);
WidgetsBinding.instance.removeObserver(this);
_clipboardStatus?.removeListener(_onChangedClipboardStatus);
_clipboardStatus?.dispose();
super.dispose();
+ assert(_batchEditDepth <= 0, 'unfinished batch edits: $_batchEditDepth');
}
// TextInputClient implementation:
- // _lastFormattedUnmodifiedTextEditingValue tracks the last value
- // that the formatter ran on and is used to prevent double-formatting.
- TextEditingValue _lastFormattedUnmodifiedTextEditingValue;
- // _lastFormattedValue tracks the last post-format value, so that it can be
- // reused without rerunning the formatter when the input value is repeated.
- TextEditingValue _lastFormattedValue;
- // _receivedRemoteTextEditingValue is the direct value last passed in
- // updateEditingValue. This value does not get updated with the formatted
- // version.
- TextEditingValue _receivedRemoteTextEditingValue;
+ /// The last known [TextEditingValue] of the platform text input plugin.
+ ///
+ /// This value is updated when the platform text input plugin sends a new
+ /// update via [updateEditingValue], or when [EditableText] calls
+ /// [TextInputConnection.setEditingState] to overwrite the platform text input
+ /// plugin's [TextEditingValue].
+ ///
+ /// Used in [_updateRemoteEditingValueIfNeeded] to determine whether the
+ /// remote value is outdated and needs updating.
+ TextEditingValue? _lastKnownRemoteTextEditingValue;
@override
TextEditingValue get currentTextEditingValue => _value;
- bool _updateEditingValueInProgress = false;
@override
void updateEditingValue(TextEditingValue value) {
- _updateEditingValueInProgress = true;
+ // This method handles text editing state updates from the platform text
// Since we still have to support keyboard select, this is the best place
// to disable text updating.
if (!_shouldCreateInputConnection) {
- _updateEditingValueInProgress = false;
return;
}
if (widget.readOnly) {
@@ -1426,69 +1623,64 @@ class ExtendedEditableTextState extends State
// everything else.
value = _value.copyWith(selection: value.selection);
}
- _receivedRemoteTextEditingValue = value;
+ _lastKnownRemoteTextEditingValue = value;
value = _handleSpecialTextSpan(value);
- if (value.text != _value.text) {
- hideToolbar();
- _showCaretOnScreen();
- _currentPromptRectRange = null;
- if (widget.obscureText && value.text.length == _value.text.length + 1) {
- _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
- _obscureLatestCharIndex = _value.selection.baseOffset;
- }
- }
-
if (value == _value) {
// This is possible, for example, when the numeric keyboard is input,
// the engine will notify twice for the same value.
// Track at https://github.com/flutter/flutter/issues/65811
- _updateEditingValueInProgress = false;
return;
}
- // else if (value.text == _value.text &&
- // value.composing == _value.composing &&
- // value.selection != _value.selection) {
- // // `selection` is the only change.
- // _handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
- // }
- else {
- _formatAndSetValue(value);
+
+ if (value.text == _value.text && value.composing == _value.composing) {
+ // `selection` is the only change.
+ _handleSelectionChanged(value.selection, SelectionChangedCause.keyboard);
+ } else {
+ hideToolbar();
+ _currentPromptRectRange = null;
+
+ if (_hasInputConnection) {
+ if (widget.obscureText && value.text.length == _value.text.length + 1) {
+ _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
+ _obscureLatestCharIndex = _value.selection.baseOffset;
+ }
+ }
+
+ _formatAndSetValue(value, SelectionChangedCause.keyboard);
}
+ // Wherever the value is changed by the user, schedule a showCaretOnScreen
+ // to make sure the user can see the changes they just made. Programmatical
+ // changes to `textEditingValue` do not trigger the behavior even if the
+ // text field is focused.
+ _scheduleShowCaretOnScreen();
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
}
- _updateEditingValueInProgress = false;
}
///zmt
TextEditingValue _handleSpecialTextSpan(TextEditingValue value) {
if (supportSpecialText) {
- final bool textChanged = _value?.text != value?.text;
- final bool selectionChanged = _value?.selection != value?.selection;
+ final bool textChanged = _value.text != value.text;
+ final bool selectionChanged = _value.selection != value.selection;
if (textChanged) {
- final TextSpan newTextSpan = widget.specialTextSpanBuilder
- .build(value?.text, textStyle: widget.style);
- if (newTextSpan == null) {
- return value;
- }
+ final TextSpan newTextSpan = widget.specialTextSpanBuilder!
+ .build(value.text, textStyle: widget.style);
- final TextSpan oldTextSpan = widget.specialTextSpanBuilder
- .build(_value?.text, textStyle: widget.style);
+ final TextSpan oldTextSpan = widget.specialTextSpanBuilder!
+ .build(_value.text, textStyle: widget.style);
value = handleSpecialTextSpanDelete(
- value, _value, oldTextSpan, _textInputConnection);
-
- if (newTextSpan != null) {
- final String text = newTextSpan.toPlainText();
- //correct caret Offset
- //make sure caret is not in text when caretIn is false
- if (text != value.text || selectionChanged) {
- value =
- correctCaretOffset(value, newTextSpan, _textInputConnection);
- }
+ value, _value, oldTextSpan, _textInputConnection!);
+
+ final String text = newTextSpan.toPlainText();
+ //correct caret Offset
+ //make sure caret is not in text when caretIn is false
+ if (text != value.text || selectionChanged) {
+ value = correctCaretOffset(value, newTextSpan, _textInputConnection!);
}
}
}
@@ -1503,9 +1695,7 @@ class ExtendedEditableTextState extends State
// If this is a multiline EditableText, do nothing for a "newline"
// action; The newline is already inserted. Otherwise, finalize
// editing.
- if (!_isMultiline) {
- _finalizeEditing(action, shouldUnfocus: true);
- }
+ if (!_isMultiline) _finalizeEditing(action, shouldUnfocus: true);
break;
case TextInputAction.done:
case TextInputAction.go:
@@ -1530,21 +1720,21 @@ class ExtendedEditableTextState extends State
@override
void performPrivateCommand(String action, Map data) {
- widget.onAppPrivateCommand(action, data);
+ widget.onAppPrivateCommand!(action, data);
}
// The original position of the caret on FloatingCursorDragState.start.
- Rect _startCaretRect;
+ Rect? _startCaretRect;
// The most recent text position as determined by the location of the floating
// cursor.
- TextPosition _lastTextPosition;
+ TextPosition? _lastTextPosition;
// The offset of the floating cursor as determined from the start call.
- Offset _pointOffsetOrigin;
+ Offset? _pointOffsetOrigin;
// The most recent position of the floating cursor.
- Offset _lastBoundedOffset;
+ Offset? _lastBoundedOffset;
// Because the center of the cursor is preferredLineHeight / 2 below the touch
// origin, but the touch origin is used to determine which line the cursor is
@@ -1554,10 +1744,13 @@ class ExtendedEditableTextState extends State
@override
void updateFloatingCursor(RawFloatingCursorPoint point) {
+ _floatingCursorResetController ??= AnimationController(
+ vsync: this,
+ )..addListener(_onFloatingCursorResetTick);
switch (point.state) {
case FloatingCursorDragState.Start:
- if (_floatingCursorResetController.isAnimating) {
- _floatingCursorResetController.stop();
+ if (_floatingCursorResetController!.isAnimating) {
+ _floatingCursorResetController!.stop();
_onFloatingCursorResetTick();
}
@@ -1566,44 +1759,44 @@ class ExtendedEditableTextState extends State
_pointOffsetOrigin = point.offset;
TextPosition currentTextPosition =
- TextPosition(offset: renderEditable.selection.baseOffset);
+ TextPosition(offset: renderEditable.selection!.baseOffset);
//zmt
if (supportSpecialText) {
currentTextPosition = convertTextInputPostionToTextPainterPostion(
- renderEditable.text, renderEditable.selection.base);
+ renderEditable.text!, renderEditable.selection!.base);
} else {
currentTextPosition =
- TextPosition(offset: renderEditable.selection.baseOffset);
+ TextPosition(offset: renderEditable.selection!.baseOffset);
}
_startCaretRect =
renderEditable.getLocalRectForCaret(currentTextPosition);
- _lastBoundedOffset = _startCaretRect.center - _floatingCursorOffset;
+ _lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
_lastTextPosition = currentTextPosition;
renderEditable.setFloatingCursor(
- point.state, _lastBoundedOffset, _lastTextPosition);
+ point.state, _lastBoundedOffset!, _lastTextPosition!);
break;
case FloatingCursorDragState.Update:
- final Offset centeredPoint = point.offset - _pointOffsetOrigin;
+ final Offset centeredPoint = point.offset! - _pointOffsetOrigin!;
final Offset rawCursorOffset =
- _startCaretRect.center + centeredPoint - _floatingCursorOffset;
+ _startCaretRect!.center + centeredPoint - _floatingCursorOffset;
_lastBoundedOffset = renderEditable
.calculateBoundedFloatingCursorOffset(rawCursorOffset);
_lastTextPosition = renderEditable.getPositionForPoint(renderEditable
- .localToGlobal(_lastBoundedOffset + _floatingCursorOffset));
- if (renderEditable?.hasSpecialInlineSpanBase ?? false) {
+ .localToGlobal(_lastBoundedOffset! + _floatingCursorOffset));
+ if (renderEditable.hasSpecialInlineSpanBase) {
_lastTextPosition = makeSureCaretNotInSpecialText(
- renderEditable.text, _lastTextPosition);
+ renderEditable.text!, _lastTextPosition!);
}
renderEditable.setFloatingCursor(
- point.state, _lastBoundedOffset, _lastTextPosition);
+ point.state, _lastBoundedOffset!, _lastTextPosition!);
break;
case FloatingCursorDragState.End:
// We skip animation if no update has happened.
if (_lastTextPosition != null && _lastBoundedOffset != null) {
- _floatingCursorResetController.value = 0.0;
- _floatingCursorResetController.animateTo(1.0,
+ _floatingCursorResetController!.value = 0.0;
+ _floatingCursorResetController!.animateTo(1.0,
duration: _floatingCursorResetTime, curve: Curves.decelerate);
}
break;
@@ -1612,38 +1805,48 @@ class ExtendedEditableTextState extends State
void _onFloatingCursorResetTick() {
final Offset finalPosition =
- renderEditable.getLocalRectForCaret(_lastTextPosition).centerLeft -
+ renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft -
_floatingCursorOffset;
- if (_floatingCursorResetController.isCompleted) {
+ if (_floatingCursorResetController!.isCompleted) {
renderEditable.setFloatingCursor(
- FloatingCursorDragState.End, finalPosition, _lastTextPosition);
- if (_lastTextPosition.offset != renderEditable.selection.baseOffset)
+ FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
+ if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset)
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
_handleSelectionChanged(
- TextSelection.collapsed(offset: _lastTextPosition.offset),
+ TextSelection.collapsed(offset: _lastTextPosition!.offset),
SelectionChangedCause.forcePress);
_startCaretRect = null;
_lastTextPosition = null;
_pointOffsetOrigin = null;
_lastBoundedOffset = null;
} else {
- final double lerpValue = _floatingCursorResetController.value;
+ final double lerpValue = _floatingCursorResetController!.value;
final double lerpX =
- ui.lerpDouble(_lastBoundedOffset.dx, finalPosition.dx, lerpValue);
+ ui.lerpDouble(_lastBoundedOffset!.dx, finalPosition.dx, lerpValue)!;
final double lerpY =
- ui.lerpDouble(_lastBoundedOffset.dy, finalPosition.dy, lerpValue);
+ ui.lerpDouble(_lastBoundedOffset!.dy, finalPosition.dy, lerpValue)!;
renderEditable.setFloatingCursor(FloatingCursorDragState.Update,
- Offset(lerpX, lerpY), _lastTextPosition,
+ Offset(lerpX, lerpY), _lastTextPosition!,
resetLerpValue: lerpValue);
}
}
- void _finalizeEditing(TextInputAction action,
- {@required bool shouldUnfocus}) {
+ @pragma('vm:notify-debugger-on-exception')
+ void _finalizeEditing(TextInputAction action, {required bool shouldUnfocus}) {
// Take any actions necessary now that the user has completed editing.
if (widget.onEditingComplete != null) {
- widget.onEditingComplete();
+ try {
+ widget.onEditingComplete!();
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context:
+ ErrorDescription('while calling onEditingComplete for $action'),
+ ));
+ }
} else {
// Default behavior if the developer did not provide an
// onEditingComplete callback: Finalize editing and remove focus, or move
@@ -1674,28 +1877,70 @@ class ExtendedEditableTextState extends State
}
}
+ final ValueChanged? onSubmitted = widget.onSubmitted;
+ if (onSubmitted == null) {
+ return;
+ }
+
// Invoke optional callback with the user's submitted content.
- if (widget.onSubmitted != null) {
- widget.onSubmitted(_value.text);
+ try {
+ onSubmitted(_value.text);
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context: ErrorDescription('while calling onSubmitted for $action'),
+ ));
}
+
+ // If `shouldUnfocus` is true, the text field should no longer be focused
+ // after the microtask queue is drained. But in case the developer cancelled
+ // the focus change in the `onSubmitted` callback by focusing this input
+ // field again, reset the soft keyboard.
+ // See https://github.com/flutter/flutter/issues/84240.
+ //
+ // `_restartConnectionIfNeeded` creates a new TextInputConnection to replace
+ // the current one. This on iOS switches to a new input view and on Android
+ // restarts the input method, and in both cases the soft keyboard will be
+ // reset.
+ if (shouldUnfocus) {
+ _scheduleRestartConnection();
+ }
+ }
+
+ int _batchEditDepth = 0;
+
+ /// Begins a new batch edit, within which new updates made to the text editing
+ /// value will not be sent to the platform text input plugin.
+ ///
+ /// Batch edits nest. When the outermost batch edit finishes, [endBatchEdit]
+ /// will attempt to send [currentTextEditingValue] to the text input plugin if
+ /// it detected a change.
+ void beginBatchEdit() {
+ _batchEditDepth += 1;
+ }
+
+ /// Ends the current batch edit started by the last call to [beginBatchEdit],
+ /// and send [currentTextEditingValue] to the text input plugin if needed.
+ ///
+ /// Throws an error in debug mode if this [EditableText] is not in a batch
+ /// edit.
+ void endBatchEdit() {
+ _batchEditDepth -= 1;
+ assert(
+ _batchEditDepth >= 0,
+ 'Unbalanced call to endBatchEdit: beginBatchEdit must be called first.',
+ );
+ _updateRemoteEditingValueIfNeeded();
}
void _updateRemoteEditingValueIfNeeded() {
- if (!_hasInputConnection) {
- return;
- }
+ if (_batchEditDepth > 0 || !_hasInputConnection) return;
final TextEditingValue localValue = _value;
- // We should not update back the value notified by the remote(engine) in reverse, this is redundant.
- // Unless we modify this value for some reason during processing, such as `TextInputFormatter`.
- if (_updateEditingValueInProgress &&
- localValue == _receivedRemoteTextEditingValue) {
- return;
- }
- // In other cases, as long as the value of the [widget.controller.value] is modified,
- // `setEditingState` should be called as we do not want to skip sending real changes
- // to the engine.
- // Also see https://github.com/flutter/flutter/issues/65059#issuecomment-690254379
- _textInputConnection.setEditingState(localValue);
+ if (localValue == _lastKnownRemoteTextEditingValue) return;
+ _textInputConnection!.setEditingState(localValue);
+ _lastKnownRemoteTextEditingValue = localValue;
}
TextEditingValue get _value => widget.controller.value;
@@ -1720,8 +1965,8 @@ class ExtendedEditableTextState extends State
return RevealedOffset(offset: _scrollController.offset, rect: rect);
final Size editableSize = renderEditable.size;
- double additionalOffset;
- Offset unitOffset;
+ final double additionalOffset;
+ final Offset unitOffset;
if (!_isMultiline) {
additionalOffset = rect.width >= editableSize.width
@@ -1729,7 +1974,7 @@ class ExtendedEditableTextState extends State
? editableSize.width / 2 - rect.center.dx
// Valid additional offsets range from (rect.right - size.width)
// to (rect.left). Pick the closest one if out of range.
- : 0.0.clamp(rect.right - editableSize.width, rect.left) as double;
+ : 0.0.clamp(rect.right - editableSize.width, rect.left);
unitOffset = const Offset(1, 0);
} else {
// The caret is vertically centered within the line. Expand the caret's
@@ -1738,14 +1983,13 @@ class ExtendedEditableTextState extends State
final Rect expandedRect = Rect.fromCenter(
center: rect.center,
width: rect.width,
- height: max(rect.height, renderEditable.preferredLineHeight),
+ height: math.max(rect.height, renderEditable.preferredLineHeight),
);
additionalOffset = expandedRect.height >= editableSize.height
? editableSize.height / 2 - expandedRect.center.dy
: 0.0.clamp(
- expandedRect.bottom - editableSize.height, expandedRect.top)
- as double;
+ expandedRect.bottom - editableSize.height, expandedRect.top);
unitOffset = const Offset(0, 1);
}
@@ -1755,18 +1999,18 @@ class ExtendedEditableTextState extends State
(additionalOffset + _scrollController.offset).clamp(
_scrollController.position.minScrollExtent,
_scrollController.position.maxScrollExtent,
- ) as double;
+ );
final double offsetDelta = _scrollController.offset - targetOffset;
return RevealedOffset(
rect: rect.shift(unitOffset * offsetDelta), offset: targetOffset);
}
- bool get _hasInputConnection =>
- _textInputConnection != null && _textInputConnection.attached;
- bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? false;
- bool get _shouldBeInAutofillContext =>
- _needsAutofill && currentAutofillScope != null;
+ bool get _hasInputConnection => _textInputConnection?.attached ?? false;
+
+ /// Whether to send the autofill information to the autofill service. True by
+ /// default.
+ bool get _needsAutofill => widget.autofillHints?.isNotEmpty ?? true;
void _openInputConnection() {
if (!_shouldCreateInputConnection) {
@@ -1774,7 +2018,6 @@ class ExtendedEditableTextState extends State
}
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
- _lastFormattedUnmodifiedTextEditingValue = localValue;
// When _needsAutofill == true && currentAutofillScope == null, autofill
// is allowed but saving the user input from the text field is
@@ -1785,20 +2028,15 @@ class ExtendedEditableTextState extends State
// notified to exclude this field from the autofill context. So we need to
// provide the autofillId.
_textInputConnection = _needsAutofill && currentAutofillScope != null
- ? currentAutofillScope.attach(this, textInputConfiguration)
+ ? currentAutofillScope!
+ .attach(this, _effectiveAutofillClient.textInputConfiguration)
: TextInput.attach(
- this,
- _createTextInputConfiguration(
- _isInAutofillContext || _needsAutofill));
- _textInputConnection.show();
+ this, _effectiveAutofillClient.textInputConfiguration);
_updateSizeAndTransform();
- if (_needsAutofill) {
- // Request autofill AFTER the size and the transform have been sent to
- // the platform text input plugin.
- _textInputConnection.requestAutofill();
- }
+ _updateComposingRectIfNeeded();
+ _updateCaretRectIfNeeded();
final TextStyle style = widget.style;
- _textInputConnection
+ _textInputConnection!
..setStyle(
fontFamily: style.fontFamily,
fontSize: style.fontSize,
@@ -1806,18 +2044,24 @@ class ExtendedEditableTextState extends State
textDirection: _textDirection,
textAlign: widget.textAlign,
)
- ..setEditingState(localValue);
+ ..setEditingState(localValue)
+ ..show();
+ if (_needsAutofill) {
+ // Request autofill AFTER the size and the transform have been sent to
+ // the platform text input plugin.
+ _textInputConnection!.requestAutofill();
+ }
+ _lastKnownRemoteTextEditingValue = localValue;
} else {
- _textInputConnection.show();
+ _textInputConnection!.show();
}
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
- _textInputConnection.close();
+ _textInputConnection!.close();
_textInputConnection = null;
- _lastFormattedUnmodifiedTextEditingValue = null;
- _receivedRemoteTextEditingValue = null;
+ _lastKnownRemoteTextEditingValue = null;
}
}
@@ -1830,13 +2074,55 @@ class ExtendedEditableTextState extends State
}
}
+ bool _restartConnectionScheduled = false;
+ void _scheduleRestartConnection() {
+ if (_restartConnectionScheduled) {
+ return;
+ }
+ _restartConnectionScheduled = true;
+ scheduleMicrotask(_restartConnectionIfNeeded);
+ }
+
+ // Discards the current [TextInputConnection] and establishes a new one.
+ //
+ // This method is rarely needed. This is currently used to reset the input
+ // type when the "submit" text input action is triggered and the developer
+ // puts the focus back to this input field..
+ void _restartConnectionIfNeeded() {
+ _restartConnectionScheduled = false;
+ if (!_hasInputConnection || !_shouldCreateInputConnection) {
+ return;
+ }
+ _textInputConnection!.close();
+ _textInputConnection = null;
+ _lastKnownRemoteTextEditingValue = null;
+
+ final AutofillScope? currentAutofillScope =
+ _needsAutofill ? this.currentAutofillScope : null;
+ final TextInputConnection newConnection = currentAutofillScope?.attach(
+ this, textInputConfiguration) ??
+ TextInput.attach(this, _effectiveAutofillClient.textInputConfiguration);
+ _textInputConnection = newConnection;
+
+ final TextStyle style = widget.style;
+ newConnection
+ ..setStyle(
+ fontFamily: style.fontFamily,
+ fontSize: style.fontSize,
+ fontWeight: style.fontWeight,
+ textDirection: _textDirection,
+ textAlign: widget.textAlign,
+ )
+ ..setEditingState(_value);
+ _lastKnownRemoteTextEditingValue = _value;
+ }
+
@override
void connectionClosed() {
if (_hasInputConnection) {
- _textInputConnection.connectionClosedReceived();
+ _textInputConnection!.connectionClosedReceived();
_textInputConnection = null;
- _lastFormattedUnmodifiedTextEditingValue = null;
- _receivedRemoteTextEditingValue = null;
+ _lastKnownRemoteTextEditingValue = null;
_finalizeEditing(TextInputAction.done, shouldUnfocus: true);
}
}
@@ -1852,23 +2138,29 @@ class ExtendedEditableTextState extends State
if (_hasFocus) {
_openInputConnection();
} else {
- widget.focusNode.requestFocus();
+ widget.focusNode
+ .requestFocus(); // This eventually calls _openInputConnection also, see _handleFocusChanged.
}
}
void _updateOrDisposeSelectionOverlayIfNeeded() {
if (_selectionOverlay != null) {
if (_hasFocus) {
- _selectionOverlay.update(_value);
+ _selectionOverlay!.update(_value);
} else {
- _selectionOverlay.dispose();
+ _selectionOverlay!.dispose();
_selectionOverlay = null;
}
}
}
+ void _updateSelectionOverlayForScroll() {
+ _selectionOverlay?.updateForScroll();
+ }
+
+ @pragma('vm:notify-debugger-on-exception')
void _handleSelectionChanged(
- TextSelection selection, SelectionChangedCause cause) {
+ TextSelection selection, SelectionChangedCause? cause) {
// We return early if the selection is not valid. This can happen when the
// text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event.
@@ -1876,17 +2168,19 @@ class ExtendedEditableTextState extends State
return;
}
- if (renderEditable?.hasSpecialInlineSpanBase ?? false) {
- final TextEditingValue value = correctCaretOffset(
- _value, renderEditable?.text, _textInputConnection,
- newSelection: selection);
-
- ///change
- if (value != _value) {
- selection = value.selection;
- _value = value;
- }
- }
+ // #172 remove this code
+ // renderEditable.text is not the same as build from _value
+ // if (renderEditable.hasSpecialInlineSpanBase) {
+ // final TextEditingValue value = correctCaretOffset(
+ // _value, renderEditable.text!, _textInputConnection,
+ // newSelection: selection);
+
+ // ///change
+ // if (value != _value) {
+ // selection = value.selection;
+ // _value = value;
+ // }
+ // }
final bool textChanged = widget.controller.text != renderEditable.plainText;
// zmt
@@ -1900,58 +2194,57 @@ class ExtendedEditableTextState extends State
// This will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures.
requestKeyboard();
-
- _hideSelectionOverlayIfNeeded();
-
- if (widget.selectionControls != null) {
- createSelectionOverlay(renderObject: renderEditable);
-
-// final bool longPress = cause == SelectionChangedCause.longPress;
-// if (cause != SelectionChangedCause.keyboard &&
-// (_value.text.isNotEmpty || longPress))
-// _selectionOverlay.showHandles();
-
+ if (widget.selectionControls == null) {
+ _selectionOverlay?.dispose();
+ _selectionOverlay = null;
+ } else {
+ if (_selectionOverlay == null) {
+ _selectionOverlay = ExtendedTextSelectionOverlay(
+ clipboardStatus: _clipboardStatus,
+ context: context,
+ value: _value,
+ debugRequiredFor: widget,
+ toolbarLayerLink: _toolbarLayerLink,
+ startHandleLayerLink: _startHandleLayerLink,
+ endHandleLayerLink: _endHandleLayerLink,
+ renderObject: renderEditable,
+ selectionControls: widget.selectionControls,
+ selectionDelegate: this,
+ dragStartBehavior: widget.dragStartBehavior,
+ onSelectionHandleTapped: widget.onSelectionHandleTapped,
+ );
+ } else {
+ _selectionOverlay!.update(_value);
+ }
+ _selectionOverlay!.handlesVisible = widget.showSelectionHandles;
+ _selectionOverlay!.showHandles();
+ }
+ // TODO(chunhtai): we should make sure selection actually changed before
+ // we call the onSelectionChanged.
+ // https://github.com/flutter/flutter/issues/76349.
+ try {
+ widget.onSelectionChanged?.call(selection, cause);
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context:
+ ErrorDescription('while calling onSelectionChanged for $cause'),
+ ));
}
- if (!textChanged && widget.onSelectionChanged != null)
- widget.onSelectionChanged(selection, cause);
- }
-
- void createSelectionOverlay({
- ExtendedRenderEditable renderObject,
- bool showHandles = true,
- }) {
- _selectionOverlay = ExtendedTextSelectionOverlay(
- clipboardStatus: _clipboardStatus,
- context: context,
- value: _value,
- debugRequiredFor: widget,
- toolbarLayerLink: _toolbarLayerLink,
- startHandleLayerLink: _startHandleLayerLink,
- endHandleLayerLink: _endHandleLayerLink,
- renderObject: renderObject ?? renderEditable,
- selectionControls: widget.selectionControls,
- selectionDelegate: this,
- dragStartBehavior: widget.dragStartBehavior,
- onSelectionHandleTapped: widget.onSelectionHandleTapped,
- );
- _selectionOverlay.handlesVisible = widget.showSelectionHandles;
- if (showHandles) {
- _selectionOverlay.showHandles();
+ // To keep the cursor from blinking while it moves, restart the timer here.
+ if (_cursorTimer != null) {
+ _stopCursorTimer(resetCharTicks: false);
+ _startCursorTimer();
}
}
- bool _textChangedSinceLastCaretUpdate = false;
- Rect _currentCaretRect;
-
+ Rect? _currentCaretRect;
+ // ignore: use_setters_to_change_properties, (this is used as a callback, can't be a setter)
void _handleCaretChanged(Rect caretRect) {
_currentCaretRect = caretRect;
- // If the caret location has changed due to an update to the text or
- // selection, then scroll the caret into view.
- if (_textChangedSinceLastCaretUpdate) {
- _textChangedSinceLastCaretUpdate = false;
- _showCaretOnScreen();
- }
}
// Animation configuration for scrolling the caret back on screen.
@@ -1960,7 +2253,7 @@ class ExtendedEditableTextState extends State
bool _showCaretOnScreenScheduled = false;
- void _showCaretOnScreen() {
+ void _scheduleShowCaretOnScreen() {
if (_showCaretOnScreenScheduled) {
return;
}
@@ -1977,20 +2270,20 @@ class ExtendedEditableTextState extends State
// positioned directly at the edge after scrolling.
double bottomSpacing = widget.scrollPadding.bottom;
if (_selectionOverlay?.selectionControls != null) {
- final double handleHeight = _selectionOverlay.selectionControls
+ final double handleHeight = _selectionOverlay!.selectionControls!
.getHandleSize(lineHeight)
.height;
- final double interactiveHandleHeight = max(
+ final double interactiveHandleHeight = math.max(
handleHeight,
kMinInteractiveDimension,
);
final Offset anchor =
- _selectionOverlay.selectionControls.getHandleAnchor(
+ _selectionOverlay!.selectionControls!.getHandleAnchor(
TextSelectionHandleType.collapsed,
lineHeight,
);
final double handleCenter = handleHeight / 2 - anchor.dy;
- bottomSpacing = max(
+ bottomSpacing = math.max(
handleCenter + interactiveHandleHeight / 2,
bottomSpacing,
);
@@ -2000,7 +2293,7 @@ class ExtendedEditableTextState extends State
widget.scrollPadding.copyWith(bottom: bottomSpacing);
final RevealedOffset targetOffset =
- _getOffsetToRevealCaret(_currentCaretRect);
+ _getOffsetToRevealCaret(_currentCaretRect!);
_scrollController.animateTo(
targetOffset.offset,
@@ -2016,80 +2309,97 @@ class ExtendedEditableTextState extends State
});
}
- double _lastBottomViewInset;
+ late double _lastBottomViewInset;
@override
void didChangeMetrics() {
- if (_lastBottomViewInset <
+ if (_lastBottomViewInset !=
WidgetsBinding.instance.window.viewInsets.bottom) {
- _showCaretOnScreen();
+ SchedulerBinding.instance.addPostFrameCallback((Duration _) {
+ _selectionOverlay?.updateForScroll();
+ });
+ if (_lastBottomViewInset <
+ WidgetsBinding.instance.window.viewInsets.bottom) {
+ _scheduleShowCaretOnScreen();
+ }
}
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
}
- _WhitespaceDirectionalityFormatter _whitespaceFormatter;
- void _formatAndSetValue(TextEditingValue value) {
- _whitespaceFormatter ??=
- _WhitespaceDirectionalityFormatter(textDirection: _textDirection);
-
- // Check if the new value is the same as the current local value, or is the same
- // as the pre-formatting value of the previous pass (repeat call).
- final bool textChanged = _value?.text != value?.text;
- final bool isRepeat = value == _lastFormattedUnmodifiedTextEditingValue;
-
- // There's no need to format when starting to compose or when continuing
- // an existing composition.
- final bool isComposing = value?.composing?.isValid ?? false;
- final bool isPreviouslyComposing =
- _lastFormattedUnmodifiedTextEditingValue?.composing?.isValid ?? false;
-
- if ((textChanged || (!isComposing && isPreviouslyComposing)) &&
- widget.inputFormatters != null &&
- widget.inputFormatters.isNotEmpty) {
- // Only format when the text has changed and there are available formatters.
- // Pass through the formatter regardless of repeat status if the input value is
- // different than the stored value.
- for (final TextInputFormatter formatter in widget.inputFormatters) {
- value = formatter.formatEditUpdate(_value, value);
- }
- // Always pass the text through the whitespace directionality formatter to
- // maintain expected behavior with carets on trailing whitespace.
- value = _whitespaceFormatter.formatEditUpdate(_value, value);
- _lastFormattedValue = value;
- }
+ @pragma('vm:notify-debugger-on-exception')
+ void _formatAndSetValue(TextEditingValue value, SelectionChangedCause? cause,
+ {bool userInteraction = false}) {
+ // Only apply input formatters if the text has changed (including uncommitted
+ // text in the composing region), or when the user committed the composing
+ // text.
+ // Gboard is very persistent in restoring the composing region. Applying
+ // input formatters on composing-region-only changes (except clearing the
+ // current composing region) is very infinite-loop-prone: the formatters
+ // will keep trying to modify the composing region while Gboard will keep
+ // trying to restore the original composing region.
+ final bool textChanged = _value.text != value.text ||
+ (!_value.composing.isCollapsed && value.composing.isCollapsed);
+ final bool selectionChanged = _value.selection != value.selection;
- //https://github.com/flutter/flutter/issues/36048
if (textChanged) {
- _hideSelectionOverlayIfNeeded();
+ try {
+ value = widget.inputFormatters?.fold(
+ value,
+ (TextEditingValue newValue, TextInputFormatter formatter) =>
+ formatter.formatEditUpdate(_value, newValue),
+ ) ??
+ value;
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context: ErrorDescription('while applying input formatters'),
+ ));
+ }
}
- // Setting _value here ensures the selection and composing region info is passed.
+ // Put all optional user callback invocations in a batch edit to prevent
+ // sending multiple `TextInput.updateEditingValue` messages.
+ beginBatchEdit();
_value = value;
- // Use the last formatted value when an identical repeat pass is detected.
- if (isRepeat && textChanged && _lastFormattedValue != null) {
- _value = _lastFormattedValue;
+ // Changes made by the keyboard can sometimes be "out of band" for listening
+ // components, so always send those events, even if we didn't think it
+ // changed. Also, the user long pressing should always send a selection change
+ // as well.
+ if (selectionChanged ||
+ (userInteraction &&
+ (cause == SelectionChangedCause.longPress ||
+ cause == SelectionChangedCause.keyboard))) {
+ _handleSelectionChanged(_value.selection, cause);
}
-
- // Always attempt to send the value. If the value has changed, then it will send,
- // otherwise, it will short-circuit.
- _updateRemoteEditingValueIfNeeded();
- if (textChanged && widget.onChanged != null) {
- widget.onChanged(value.text);
+ if (textChanged) {
+ try {
+ widget.onChanged?.call(_value.text);
+ } catch (exception, stack) {
+ FlutterError.reportError(FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets',
+ context: ErrorDescription('while calling onChanged'),
+ ));
+ }
}
- _lastFormattedUnmodifiedTextEditingValue = _receivedRemoteTextEditingValue;
+
+ endBatchEdit();
}
void _onCursorColorTick() {
renderEditable.cursorColor =
- widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
+ widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
_cursorVisibilityNotifier.value =
- widget.showCursor && _cursorBlinkOpacityController.value > 0;
+ widget.showCursor && _cursorBlinkOpacityController!.value > 0;
}
/// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks).
@visibleForTesting
- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController!.value > 0;
/// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this
@@ -2099,10 +2409,10 @@ class ExtendedEditableTextState extends State
/// The current status of the text selection handles.
//@visibleForTesting
- ExtendedTextSelectionOverlay get selectionOverlay => _selectionOverlay;
+ ExtendedTextSelectionOverlay? get selectionOverlay => _selectionOverlay;
int _obscureShowCharTicksPending = 0;
- int _obscureLatestCharIndex;
+ int? _obscureLatestCharIndex;
void _cursorTick(Timer timer) {
_targetCursorVisibility = !_targetCursorVisibility;
@@ -2115,10 +2425,10 @@ class ExtendedEditableTextState extends State
//
// These values and curves have been obtained through eyeballing, so are
// likely not exactly the same as the values for native iOS.
- _cursorBlinkOpacityController.animateTo(targetOpacity,
- curve: Curves.easeOut);
+ _cursorBlinkOpacityController!
+ .animateTo(targetOpacity, curve: Curves.easeOut);
} else {
- _cursorBlinkOpacityController.value = targetOpacity;
+ _cursorBlinkOpacityController!.value = targetOpacity;
}
if (_obscureShowCharTicksPending > 0) {
@@ -2130,16 +2440,24 @@ class ExtendedEditableTextState extends State
void _cursorWaitForStart(Timer timer) {
assert(_kCursorBlinkHalfPeriod > _fadeDuration);
+ assert(!EditableText.debugDeterministicCursor);
_cursorTimer?.cancel();
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
+ // Indicates whether the cursor should be blinking right now (but it may
+ // actually not blink because it's disabled via TickerMode.of(context)).
+ bool _cursorActive = false;
+
void _startCursorTimer() {
- _targetCursorVisibility = true;
- _cursorBlinkOpacityController.value = 1.0;
- if (ExtendedEditableText.debugDeterministicCursor) {
+ assert(_cursorTimer == null);
+ _cursorActive = true;
+ if (!_tickersEnabled) {
return;
}
+ _targetCursorVisibility = true;
+ _cursorBlinkOpacityController!.value = 1.0;
+ if (EditableText.debugDeterministicCursor) return;
if (widget.cursorOpacityAnimates) {
_cursorTimer =
Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
@@ -2149,39 +2467,34 @@ class ExtendedEditableTextState extends State
}
void _stopCursorTimer({bool resetCharTicks = true}) {
+ _cursorActive = false;
_cursorTimer?.cancel();
_cursorTimer = null;
_targetCursorVisibility = false;
- _cursorBlinkOpacityController.value = 0.0;
- if (ExtendedEditableText.debugDeterministicCursor) {
- return;
- }
- if (resetCharTicks) {
- _obscureShowCharTicksPending = 0;
- }
+ _cursorBlinkOpacityController!.value = 0.0;
+ if (EditableText.debugDeterministicCursor) return;
+ if (resetCharTicks) _obscureShowCharTicksPending = 0;
if (widget.cursorOpacityAnimates) {
- _cursorBlinkOpacityController.stop();
- _cursorBlinkOpacityController.value = 0.0;
+ _cursorBlinkOpacityController!.stop();
+ _cursorBlinkOpacityController!.value = 0.0;
}
}
void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed)
_startCursorTimer();
- else if (_cursorTimer != null &&
- (!_hasFocus || !_value.selection.isCollapsed)) {
+ else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed))
_stopCursorTimer();
- }
}
void _didChangeTextEditingValue() {
_updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
- _textChangedSinceLastCaretUpdate = true;
// TODO(abarth): Teach RenderEditable about ValueNotifier
// to avoid this setState().
setState(() {/* We use widget.controller.value in build(). */});
+ _adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges();
}
void _handleFocusChanged() {
@@ -2192,18 +2505,20 @@ class ExtendedEditableTextState extends State
// Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom;
- _showCaretOnScreen();
+ if (!widget.readOnly) {
+ _scheduleShowCaretOnScreen();
+ }
if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
- widget.controller.selection =
- TextSelection.collapsed(offset: _value.text.length);
+ _handleSelectionChanged(
+ TextSelection.collapsed(offset: _value.text.length), null);
}
} else {
WidgetsBinding.instance.removeObserver(this);
- // Clear the selection and composition state if this widget lost focus.
- _value = TextEditingValue(text: _value.text);
- _currentPromptRectRange = null;
- }
+ setState(() {
+ _currentPromptRectRange = null;
+ });
+ }
updateKeepAlive();
}
@@ -2211,9 +2526,53 @@ class ExtendedEditableTextState extends State
if (_hasInputConnection) {
final Size size = renderEditable.size;
final Matrix4 transform = renderEditable.getTransformTo(null);
- _textInputConnection.setEditableSizeAndTransform(size, transform);
+ _textInputConnection!.setEditableSizeAndTransform(size, transform);
+ _updateSelectionRects();
SchedulerBinding.instance
.addPostFrameCallback((Duration _) => _updateSizeAndTransform());
+ } else if (_placeholderLocation != -1) {
+ removeTextPlaceholder();
+ }
+ }
+
+ // Sends the current composing rect to the iOS text input plugin via the text
+ // input channel. We need to keep sending the information even if no text is
+ // currently marked, as the information usually lags behind. The text input
+ // plugin needs to estimate the composing rect based on the latest caret rect,
+ // when the composing rect info didn't arrive in time.
+ void _updateComposingRectIfNeeded() {
+ final TextRange composingRange = _value.composing;
+ if (_hasInputConnection) {
+ assert(mounted);
+ Rect? composingRect =
+ renderEditable.getRectForComposingRange(composingRange);
+ // Send the caret location instead if there's no marked text yet.
+ if (composingRect == null) {
+ assert(!composingRange.isValid || composingRange.isCollapsed);
+ final int offset = composingRange.isValid ? composingRange.start : 0;
+ composingRect =
+ renderEditable.getLocalRectForCaret(TextPosition(offset: offset));
+ }
+ assert(composingRect != null);
+ _textInputConnection!.setComposingRect(composingRect);
+ SchedulerBinding.instance
+ .addPostFrameCallback((Duration _) => _updateComposingRectIfNeeded());
+ }
+ }
+
+ void _updateCaretRectIfNeeded() {
+ if (_hasInputConnection) {
+ if (renderEditable.selection != null &&
+ renderEditable.selection!.isValid &&
+ renderEditable.selection!.isCollapsed) {
+ final TextPosition currentTextPosition =
+ TextPosition(offset: renderEditable.selection!.baseOffset);
+ final Rect caretRect =
+ renderEditable.getLocalRectForCaret(currentTextPosition);
+ _textInputConnection!.setCaretRect(caretRect);
+ }
+ SchedulerBinding.instance
+ .addPostFrameCallback((Duration _) => _updateCaretRectIfNeeded());
}
}
@@ -2230,27 +2589,38 @@ class ExtendedEditableTextState extends State
/// This property is typically used to notify the renderer of input gestures
/// when [ignorePointer] is true. See [RenderEditable.ignorePointer].
ExtendedRenderEditable get renderEditable =>
- _editableKey.currentContext.findRenderObject() as ExtendedRenderEditable;
+ _editableKey.currentContext!.findRenderObject() as ExtendedRenderEditable;
@override
TextEditingValue get textEditingValue => _value;
- double get _devicePixelRatio =>
- MediaQuery.of(context).devicePixelRatio ?? 1.0;
+ double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio;
@override
- set textEditingValue(TextEditingValue value) {
+ void userUpdateTextEditingValue(
+ TextEditingValue value, SelectionChangedCause? cause) {
value = _handleSpecialTextSpan(value);
- _selectionOverlay?.update(value);
- _formatAndSetValue(value);
+ // Compare the current TextEditingValue with the pre-format new
+ // TextEditingValue value, in case the formatter would reject the change.
+ final bool shouldShowCaret =
+ widget.readOnly ? _value.selection != value.selection : _value != value;
+ if (shouldShowCaret) {
+ _scheduleShowCaretOnScreen();
+ }
+ _formatAndSetValue(value, cause, userInteraction: true);
}
@override
- void bringIntoView(TextPosition position) {
+ void bringIntoView(TextPosition position, {double offset = 0}) {
+ if (supportSpecialText) {
+ position = convertTextInputPostionToTextPainterPostion(
+ renderEditable.text!, position);
+ }
+
final Rect localRect = renderEditable.getLocalRectForCaret(position);
- final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect);
- _scrollController.jumpTo(targetOffset.offset);
+ final RevealedOffset targetOffset = _getOffsetToRevealCaret(localRect);
+ _scrollController.jumpTo(targetOffset.offset + offset);
renderEditable.showOnScreen(rect: targetOffset.rect);
}
@@ -2258,67 +2628,95 @@ class ExtendedEditableTextState extends State
///
/// Returns `false` if a toolbar couldn't be shown, such as when the toolbar
/// is already shown, or when no text selection currently exists.
- bool showToolbar() {
+ @override
+ bool showToolbar({bool showToolbarInWeb = false}) {
// Web is using native dom elements to enable clipboard functionality of the
// toolbar: copy, paste, select, cut. It might also provide additional
// functionality depending on the browser (such as translate). Due to this
// we should not show a Flutter toolbar for the editable text elements.
- if (kIsWeb) {
+ if (kIsWeb && !showToolbarInWeb) {
return false;
}
- if (_selectionOverlay == null &&
- FocusScope.of(context).focusedChild == widget.focusNode) {
- createSelectionOverlay();
- }
-
- if (_selectionOverlay == null || _selectionOverlay.toolbarIsVisible) {
+ if (_selectionOverlay == null || _selectionOverlay!.toolbarIsVisible) {
return false;
}
- _selectionOverlay.showToolbar();
+ _selectionOverlay!.showToolbar();
return true;
}
- void _hideSelectionOverlayIfNeeded() {
- _selectionOverlay?.hide();
- _selectionOverlay = null;
- }
-
@override
- void hideToolbar() {
- _selectionOverlay?.hide();
+ void hideToolbar([bool hideHandles = true]) {
+ if (hideHandles) {
+ // Hide the handles and the toolbar.
+ _selectionOverlay?.hide();
+ } else if (_selectionOverlay?.toolbarIsVisible ?? false) {
+ // Hide only the toolbar but not the handles.
+ _selectionOverlay?.hideToolbar();
+ }
}
/// Toggles the visibility of the toolbar.
void toggleToolbar() {
assert(_selectionOverlay != null);
- if (_selectionOverlay.toolbarIsVisible) {
+ if (_selectionOverlay!.toolbarIsVisible) {
hideToolbar();
} else {
showToolbar();
}
}
+ // Tracks the location a [_ScribblePlaceholder] should be rendered in the
+ // text.
+ //
+ // A value of -1 indicates there should be no placeholder, otherwise the
+ // value should be between 0 and the length of the text, inclusive.
+ int _placeholderLocation = -1;
+
+ @override
+ void insertTextPlaceholder(Size size) {
+ if (!widget.scribbleEnabled) return;
+
+ if (!widget.controller.selection.isValid) return;
+
+ setState(() {
+ _placeholderLocation =
+ _value.text.length - widget.controller.selection.end;
+ });
+ }
+
+ @override
+ void removeTextPlaceholder() {
+ if (!widget.scribbleEnabled) return;
+
+ setState(() {
+ _placeholderLocation = -1;
+ });
+ }
+
@override
String get autofillId => 'EditableText-$hashCode';
- TextInputConfiguration _createTextInputConfiguration(
- bool needsAutofillConfiguration) {
- assert(needsAutofillConfiguration != null);
+ @override
+ TextInputConfiguration get textInputConfiguration {
+ final List? autofillHints =
+ widget.autofillHints?.toList(growable: false);
+ final AutofillConfiguration autofillConfiguration = autofillHints != null
+ ? AutofillConfiguration(
+ uniqueIdentifier: autofillId,
+ autofillHints: autofillHints,
+ currentEditingValue: currentTextEditingValue,
+ )
+ : AutofillConfiguration.disabled;
+
return TextInputConfiguration(
inputType: widget.keyboardType,
readOnly: widget.readOnly,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
- smartDashesType: widget.smartDashesType ??
- (widget.obscureText
- ? SmartDashesType.disabled
- : SmartDashesType.enabled),
- smartQuotesType: widget.smartQuotesType ??
- (widget.obscureText
- ? SmartQuotesType.disabled
- : SmartQuotesType.enabled),
+ smartDashesType: widget.smartDashesType,
+ smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
inputAction: widget.textInputAction ??
(widget.keyboardType == TextInputType.multiline
@@ -2326,24 +2724,16 @@ class ExtendedEditableTextState extends State
: TextInputAction.done),
textCapitalization: widget.textCapitalization,
keyboardAppearance: widget.keyboardAppearance,
- autofillConfiguration: !needsAutofillConfiguration
- ? null
- : AutofillConfiguration(
- uniqueIdentifier: autofillId,
- autofillHints:
- widget.autofillHints?.toList(growable: false) ?? [],
- currentEditingValue: currentTextEditingValue,
- ),
+ autofillConfiguration: autofillConfiguration,
+ enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
);
}
@override
- TextInputConfiguration get textInputConfiguration {
- return _createTextInputConfiguration(_needsAutofill);
- }
+ void autofill(TextEditingValue value) => updateEditingValue(value);
// null if no promptRect should be shown.
- TextRange _currentPromptRectRange;
+ TextRange? _currentPromptRectRange;
@override
void showAutocorrectionPromptRect(int start, int end) {
@@ -2352,123 +2742,274 @@ class ExtendedEditableTextState extends State
});
}
- VoidCallback _semanticsOnCopy(TextSelectionControls controls) {
+ VoidCallback? _semanticsOnCopy(TextSelectionControls? controls) {
return widget.selectionEnabled &&
copyEnabled &&
_hasFocus &&
controls?.canCopy(this) == true
- ? () => controls.handleCopy(this, _clipboardStatus)
+ ? () => controls!.handleCopy(this, _clipboardStatus)
: null;
}
- VoidCallback _semanticsOnCut(TextSelectionControls controls) {
+ VoidCallback? _semanticsOnCut(TextSelectionControls? controls) {
return widget.selectionEnabled &&
cutEnabled &&
_hasFocus &&
controls?.canCut(this) == true
- ? () => controls.handleCut(this)
+ ? () => controls!.handleCut(this, _clipboardStatus)
: null;
}
- VoidCallback _semanticsOnPaste(TextSelectionControls controls) {
+ VoidCallback? _semanticsOnPaste(TextSelectionControls? controls) {
return widget.selectionEnabled &&
pasteEnabled &&
_hasFocus &&
controls?.canPaste(this) == true &&
(_clipboardStatus == null ||
- _clipboardStatus.value == ClipboardStatus.pasteable)
- ? () => controls.handlePaste(this)
+ _clipboardStatus!.value == ClipboardStatus.pasteable)
+ ? () => controls!.handlePaste(this)
: null;
}
+ // --------------------------- Text Editing Actions ---------------------------
+
+ _TextBoundary _characterBoundary(DirectionalTextEditingIntent intent) {
+ final _TextBoundary atomicTextBoundary = widget.obscureText
+ ? _CodeUnitBoundary(_value)
+ : _CharacterBoundary(_value);
+ return _CollapsedSelectionBoundary(atomicTextBoundary, intent.forward);
+ }
+
+ _TextBoundary _nextWordBoundary(DirectionalTextEditingIntent intent) {
+ final _TextBoundary atomicTextBoundary;
+ final _TextBoundary boundary;
+
+ if (widget.obscureText) {
+ atomicTextBoundary = _CodeUnitBoundary(_value);
+ boundary = _DocumentBoundary(_value);
+ } else {
+ final TextEditingValue textEditingValue =
+ _textEditingValueforTextLayoutMetrics;
+ atomicTextBoundary = _CharacterBoundary(textEditingValue);
+ // This isn't enough. Newline characters.
+ boundary = _ExpandedTextBoundary(_WhitespaceBoundary(textEditingValue),
+ _WordBoundary(renderEditable, textEditingValue));
+ }
+
+ final _MixedBoundary mixedBoundary = intent.forward
+ ? _MixedBoundary(atomicTextBoundary, boundary)
+ : _MixedBoundary(boundary, atomicTextBoundary);
+ // Use a _MixedBoundary to make sure we don't leave invalid codepoints in
+ // the field after deletion.
+ return _CollapsedSelectionBoundary(mixedBoundary, intent.forward);
+ }
+
+ _TextBoundary _linebreak(DirectionalTextEditingIntent intent) {
+ final _TextBoundary atomicTextBoundary;
+ final _TextBoundary boundary;
+
+ if (widget.obscureText) {
+ atomicTextBoundary = _CodeUnitBoundary(_value);
+ boundary = _DocumentBoundary(_value);
+ } else {
+ final TextEditingValue textEditingValue =
+ _textEditingValueforTextLayoutMetrics;
+ atomicTextBoundary = _CharacterBoundary(textEditingValue);
+ boundary = _LineBreak(renderEditable, textEditingValue);
+ }
+
+ // The _MixedBoundary is to make sure we don't leave invalid code units in
+ // the field after deletion.
+ // `boundary` doesn't need to be wrapped in a _CollapsedSelectionBoundary,
+ // since the document boundary is unique and the linebreak boundary is
+ // already caret-location based.
+ return intent.forward
+ ? _MixedBoundary(
+ _CollapsedSelectionBoundary(atomicTextBoundary, true), boundary)
+ : _MixedBoundary(
+ boundary, _CollapsedSelectionBoundary(atomicTextBoundary, false));
+ }
+
+ _TextBoundary _documentBoundary(DirectionalTextEditingIntent intent) =>
+ _DocumentBoundary(_value);
+
+ Action _makeOverridable(Action defaultAction) {
+ return Action.overridable(
+ context: context, defaultAction: defaultAction);
+ }
+
+ void _replaceText(ReplaceTextIntent intent) {
+ userUpdateTextEditingValue(
+ intent.currentTextEditingValue
+ .replaced(intent.replacementRange, intent.replacementText),
+ intent.cause,
+ );
+ }
+
+ late final Action _replaceTextAction =
+ CallbackAction(onInvoke: _replaceText);
+
+ void _updateSelection(UpdateSelectionIntent intent) {
+ userUpdateTextEditingValue(
+ intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
+ intent.cause,
+ );
+ }
+
+ late final Action _updateSelectionAction =
+ CallbackAction(onInvoke: _updateSelection);
+
+ late final _UpdateTextSelectionToAdjacentLineAction<
+ ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction =
+ _UpdateTextSelectionToAdjacentLineAction<
+ ExtendSelectionVerticallyToAdjacentLineIntent>(this);
+
+ late final Map> _actions = >{
+ DoNothingAndStopPropagationTextIntent: DoNothingAction(consumesKey: false),
+ ReplaceTextIntent: _replaceTextAction,
+ UpdateSelectionIntent: _updateSelectionAction,
+ DirectionalFocusIntent: DirectionalFocusAction.forTextField(),
+
+ // Delete
+ DeleteCharacterIntent: _makeOverridable(
+ _DeleteTextAction(this, _characterBoundary)),
+ DeleteToNextWordBoundaryIntent: _makeOverridable(
+ _DeleteTextAction(
+ this, _nextWordBoundary)),
+ DeleteToLineBreakIntent: _makeOverridable(
+ _DeleteTextAction