zefyr

input.dart 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file
  2. // for details. All rights reserved. Use of this source code is governed by a
  3. // BSD-style license that can be found in the LICENSE file.
  4. import 'package:flutter/services.dart';
  5. import 'package:flutter/widgets.dart';
  6. import 'package:zefyr/util.dart';
  7. typedef RemoteValueChanged = Function(
  8. int start, String deleted, String inserted, TextSelection selection);
  9. class InputConnectionController implements TextInputClient {
  10. InputConnectionController(this.onValueChanged)
  11. : assert(onValueChanged != null);
  12. //
  13. // public members
  14. //
  15. final RemoteValueChanged onValueChanged;
  16. /// Returns `true` if there is open input connection.
  17. bool get hasConnection =>
  18. _textInputConnection != null && _textInputConnection.attached;
  19. /// Opens or closes input connection based on the current state of
  20. /// [focusNode] and [value].
  21. void openOrCloseConnection(FocusNode focusNode, TextEditingValue value) {
  22. if (focusNode.hasFocus && focusNode.consumeKeyboardToken()) {
  23. openConnection(value);
  24. } else if (!focusNode.hasFocus) {
  25. closeConnection();
  26. }
  27. }
  28. void openConnection(TextEditingValue value) {
  29. if (!hasConnection) {
  30. _lastKnownRemoteTextEditingValue = value;
  31. _textInputConnection = TextInput.attach(
  32. this,
  33. TextInputConfiguration(
  34. inputType: TextInputType.multiline,
  35. obscureText: false,
  36. autocorrect: true,
  37. inputAction: TextInputAction.newline,
  38. textCapitalization: TextCapitalization.sentences,
  39. ),
  40. )..setEditingState(value);
  41. _sentRemoteValues.add(value);
  42. }
  43. _textInputConnection.show();
  44. }
  45. /// Closes input connection if it's currently open. Otherwise does nothing.
  46. void closeConnection() {
  47. if (hasConnection) {
  48. _textInputConnection.close();
  49. _textInputConnection = null;
  50. _lastKnownRemoteTextEditingValue = null;
  51. _sentRemoteValues.clear();
  52. }
  53. }
  54. /// Updates remote value based on current state of [document] and
  55. /// [selection].
  56. ///
  57. /// This method may not actually send an update to native side if it thinks
  58. /// remote value is up to date or identical.
  59. void updateRemoteValue(TextEditingValue value) {
  60. if (!hasConnection) return;
  61. // Since we don't keep track of composing range in value provided by
  62. // ZefyrController we need to add it here manually before comparing
  63. // with the last known remote value.
  64. // It is important to prevent excessive remote updates as it can cause
  65. // race conditions.
  66. final actualValue = value.copyWith(
  67. composing: _lastKnownRemoteTextEditingValue.composing,
  68. );
  69. if (actualValue == _lastKnownRemoteTextEditingValue) return;
  70. bool shouldRemember = value.text != _lastKnownRemoteTextEditingValue.text;
  71. _lastKnownRemoteTextEditingValue = actualValue;
  72. _textInputConnection.setEditingState(actualValue);
  73. if (shouldRemember) {
  74. // Only keep track if text changed (selection changes are not relevant)
  75. _sentRemoteValues.add(actualValue);
  76. }
  77. }
  78. //
  79. // Overridden members
  80. //
  81. @override
  82. void performAction(TextInputAction action) {
  83. // no-op
  84. }
  85. @override
  86. void updateEditingValue(TextEditingValue value) {
  87. if (_sentRemoteValues.contains(value)) {
  88. /// There is a race condition in Flutter text input plugin where sending
  89. /// updates to native side too often results in broken behavior.
  90. /// TextInputConnection.setEditingValue is an async call to native side.
  91. /// For each such call native side _always_ sends update which triggers
  92. /// this method (updateEditingValue) with the same value we've sent it.
  93. /// If multiple calls to setEditingValue happen too fast and we only
  94. /// track the last sent value then there is no way for us to filter out
  95. /// automatic callbacks from native side.
  96. /// Therefore we have to keep track of all values we send to the native
  97. /// side and when we see this same value appear here we skip it.
  98. /// This is fragile but it's probably the only available option.
  99. _sentRemoteValues.remove(value);
  100. return;
  101. }
  102. if (_lastKnownRemoteTextEditingValue == value) {
  103. // There is no difference between this value and the last known value.
  104. return;
  105. }
  106. // Check if only composing range changed.
  107. if (_lastKnownRemoteTextEditingValue.text == value.text &&
  108. _lastKnownRemoteTextEditingValue.selection == value.selection) {
  109. // This update only modifies composing range. Since we don't keep track
  110. // of composing range in Zefyr we just need to update last known value
  111. // here.
  112. // Note: this check fixes an issue on Android when it sends
  113. // composing updates separately from regular changes for text and
  114. // selection.
  115. _lastKnownRemoteTextEditingValue = value;
  116. return;
  117. }
  118. // Note Flutter (unintentionally?) silences errors occurred during
  119. // text input update, so we have to report it ourselves.
  120. // For more details see https://github.com/flutter/flutter/issues/19191
  121. // TODO: remove try-catch when/if Flutter stops silencing these errors.
  122. try {
  123. final effectiveLastKnownValue = _lastKnownRemoteTextEditingValue;
  124. _lastKnownRemoteTextEditingValue = value;
  125. final oldText = effectiveLastKnownValue.text;
  126. final text = value.text;
  127. final cursorPosition = value.selection.extentOffset;
  128. final diff = fastDiff(oldText, text, cursorPosition);
  129. onValueChanged(diff.start, diff.deleted, diff.inserted, value.selection);
  130. } catch (e, trace) {
  131. FlutterError.reportError(FlutterErrorDetails(
  132. exception: e,
  133. stack: trace,
  134. library: 'Zefyr',
  135. context: ErrorSummary('while updating editing value'),
  136. ));
  137. rethrow;
  138. }
  139. }
  140. //
  141. // Private members
  142. //
  143. final List<TextEditingValue> _sentRemoteValues = [];
  144. TextInputConnection _textInputConnection;
  145. TextEditingValue _lastKnownRemoteTextEditingValue;
  146. @override
  147. void updateFloatingCursor(RawFloatingCursorPoint point) {
  148. // TODO: implement updateFloatingCursor
  149. }
  150. @override
  151. void connectionClosed() {
  152. if (hasConnection) {
  153. _textInputConnection.connectionClosedReceived();
  154. _textInputConnection = null;
  155. _lastKnownRemoteTextEditingValue = null;
  156. _sentRemoteValues.clear();
  157. }
  158. }
  159. }