Update to new editing.mojom
Now the keyboard is responsible for maintaining the state of the text field.
This commit is contained in:
parent
9e784f0cd3
commit
fade0dc097
@ -6,6 +6,7 @@ import 'package:flutter/animation.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
|
||||||
|
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
@ -13,7 +14,7 @@ import 'icon.dart';
|
|||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
export 'package:flutter/rendering.dart' show ValueChanged;
|
export 'package:flutter/rendering.dart' show ValueChanged;
|
||||||
export 'package:flutter/services.dart' show KeyboardType;
|
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
|
||||||
|
|
||||||
/// A material design text input field.
|
/// A material design text input field.
|
||||||
class Input extends StatefulComponent {
|
class Input extends StatefulComponent {
|
||||||
@ -110,10 +111,11 @@ class _InputState extends State<Input> {
|
|||||||
|
|
||||||
void _attachOrDetachKeyboard(bool focused) {
|
void _attachOrDetachKeyboard(bool focused) {
|
||||||
if (focused && !_isAttachedToKeyboard) {
|
if (focused && !_isAttachedToKeyboard) {
|
||||||
_keyboardHandle = keyboard.show(_editableString.createStub(), config.keyboardType);
|
_keyboardHandle = keyboard.attach(_editableString.createStub(),
|
||||||
_keyboardHandle.setText(_editableString.text);
|
new mojom.KeyboardConfiguration()
|
||||||
_keyboardHandle.setSelection(_editableString.selection.start,
|
..type = config.keyboardType);
|
||||||
_editableString.selection.end);
|
_keyboardHandle.setEditingState(_editableString.editingState);
|
||||||
|
_keyboardHandle.show();
|
||||||
} else if (!focused && _isAttachedToKeyboard) {
|
} else if (!focused && _isAttachedToKeyboard) {
|
||||||
_keyboardHandle.release();
|
_keyboardHandle.release();
|
||||||
_keyboardHandle = null;
|
_keyboardHandle = null;
|
||||||
@ -124,7 +126,7 @@ class _InputState extends State<Input> {
|
|||||||
void _requestKeyboard() {
|
void _requestKeyboard() {
|
||||||
if (Focus.at(context)) {
|
if (Focus.at(context)) {
|
||||||
assert(_isAttachedToKeyboard);
|
assert(_isAttachedToKeyboard);
|
||||||
_keyboardHandle.showByRequest();
|
_keyboardHandle.show();
|
||||||
} else {
|
} else {
|
||||||
Focus.moveTo(config.key);
|
Focus.moveTo(config.key);
|
||||||
// we'll get told to rebuild and we'll take care of the keyboard then
|
// we'll get told to rebuild and we'll take care of the keyboard then
|
||||||
@ -149,7 +151,8 @@ class _InputState extends State<Input> {
|
|||||||
|
|
||||||
void _handleSelectionChanged(TextSelection selection) {
|
void _handleSelectionChanged(TextSelection selection) {
|
||||||
if (_isAttachedToKeyboard) {
|
if (_isAttachedToKeyboard) {
|
||||||
_keyboardHandle.setSelection(selection.start, selection.end);
|
_editableString.setSelection(selection);
|
||||||
|
_keyboardHandle.setEditingState(_editableString.editingState);
|
||||||
} else {
|
} else {
|
||||||
_editableString.setSelection(selection);
|
_editableString.setSelection(selection);
|
||||||
_requestKeyboard();
|
_requestKeyboard();
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
|
||||||
|
|
||||||
import 'shell.dart';
|
import 'shell.dart';
|
||||||
|
|
||||||
export 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
|
||||||
|
|
||||||
/// An interface to the system's keyboard.
|
/// An interface to the system's keyboard.
|
||||||
///
|
///
|
||||||
@ -19,22 +19,24 @@ class Keyboard {
|
|||||||
// The service is exposed in case you need direct access.
|
// The service is exposed in case you need direct access.
|
||||||
// However, as a general rule, you should be able to do
|
// However, as a general rule, you should be able to do
|
||||||
// most of what you need using only this class.
|
// most of what you need using only this class.
|
||||||
final KeyboardService service;
|
final mojom.Keyboard service;
|
||||||
|
|
||||||
KeyboardHandle _currentHandle;
|
KeyboardHandle _currentHandle;
|
||||||
|
|
||||||
bool _hidePending = false;
|
bool _hidePending = false;
|
||||||
|
|
||||||
KeyboardHandle show(KeyboardClientStub stub, KeyboardType keyboardType) {
|
KeyboardHandle attach(mojom.KeyboardClientStub stub, mojom.KeyboardConfiguration configuration) {
|
||||||
assert(stub != null);
|
assert(stub != null);
|
||||||
_currentHandle?.release();
|
_currentHandle?.release();
|
||||||
assert(_currentHandle == null);
|
assert(_currentHandle == null);
|
||||||
_currentHandle = new KeyboardHandle._show(this, stub, keyboardType);
|
_currentHandle = new KeyboardHandle._(this);
|
||||||
|
service.setClient(stub, configuration);
|
||||||
return _currentHandle;
|
return _currentHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _scheduleHide() {
|
void _scheduleHide() {
|
||||||
if (_hidePending) return;
|
if (_hidePending)
|
||||||
|
return;
|
||||||
_hidePending = true;
|
_hidePending = true;
|
||||||
|
|
||||||
// Schedule a deferred task that hides the keyboard. If someone else shows
|
// Schedule a deferred task that hides the keyboard. If someone else shows
|
||||||
@ -50,21 +52,17 @@ class Keyboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class KeyboardHandle {
|
class KeyboardHandle {
|
||||||
|
KeyboardHandle._(Keyboard keyboard) : _keyboard = keyboard, _attached = true;
|
||||||
KeyboardHandle._show(Keyboard keyboard, KeyboardClientStub stub, KeyboardType keyboardType) : _keyboard = keyboard {
|
|
||||||
_keyboard.service.show(stub, keyboardType);
|
|
||||||
_attached = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Keyboard _keyboard;
|
final Keyboard _keyboard;
|
||||||
|
|
||||||
bool _attached;
|
bool _attached;
|
||||||
bool get attached => _attached;
|
bool get attached => _attached;
|
||||||
|
|
||||||
void showByRequest() {
|
void show() {
|
||||||
assert(_attached);
|
assert(_attached);
|
||||||
assert(_keyboard._currentHandle == this);
|
assert(_keyboard._currentHandle == this);
|
||||||
_keyboard.service.showByRequest();
|
_keyboard.service.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void release() {
|
void release() {
|
||||||
@ -77,25 +75,18 @@ class KeyboardHandle {
|
|||||||
assert(_keyboard._currentHandle != this);
|
assert(_keyboard._currentHandle != this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setText(String text) {
|
void setEditingState(mojom.EditingState state) {
|
||||||
assert(_attached);
|
assert(_attached);
|
||||||
assert(_keyboard._currentHandle == this);
|
assert(_keyboard._currentHandle == this);
|
||||||
_keyboard.service.setText(text);
|
_keyboard.service.setEditingState(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSelection(int start, int end) {
|
|
||||||
assert(_attached);
|
|
||||||
assert(_keyboard._currentHandle == this);
|
|
||||||
_keyboard.service.setSelection(start, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyboardServiceProxy _initKeyboardProxy() {
|
mojom.KeyboardProxy _initKeyboardProxy() {
|
||||||
KeyboardServiceProxy proxy = new KeyboardServiceProxy.unbound();
|
mojom.KeyboardProxy proxy = new mojom.KeyboardProxy.unbound();
|
||||||
shell.connectToService(null, proxy);
|
shell.connectToService(null, proxy);
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
final KeyboardServiceProxy _keyboardProxy = _initKeyboardProxy();
|
final mojom.KeyboardProxy _keyboardProxy = _initKeyboardProxy();
|
||||||
final Keyboard keyboard = new Keyboard(_keyboardProxy.ptr);
|
final Keyboard keyboard = new Keyboard(_keyboardProxy.ptr);
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
@ -18,7 +17,16 @@ export 'package:flutter/painting.dart' show TextSelection;
|
|||||||
|
|
||||||
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
|
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
|
||||||
|
|
||||||
class _KeyboardClientImpl implements KeyboardClient {
|
TextSelection _getTextSelectionFromEditingState(mojom.EditingState state) {
|
||||||
|
return new TextSelection(
|
||||||
|
baseOffset: state.selectionBase,
|
||||||
|
extentOffset: state.selectionExtent,
|
||||||
|
affinity: TextAffinity.values[state.selectionAffinity.mojoEnumValue],
|
||||||
|
isDirectional: state.selectionIsDirectional
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KeyboardClientImpl implements mojom.KeyboardClient {
|
||||||
_KeyboardClientImpl({
|
_KeyboardClientImpl({
|
||||||
String text: '',
|
String text: '',
|
||||||
TextSelection selection,
|
TextSelection selection,
|
||||||
@ -45,86 +53,29 @@ class _KeyboardClientImpl implements KeyboardClient {
|
|||||||
TextSelection selection;
|
TextSelection selection;
|
||||||
|
|
||||||
/// A keyboard client stub that can be attached to a keyboard service.
|
/// A keyboard client stub that can be attached to a keyboard service.
|
||||||
KeyboardClientStub createStub() {
|
mojom.KeyboardClientStub createStub() {
|
||||||
return new KeyboardClientStub.unbound()..impl = this;
|
return new mojom.KeyboardClientStub.unbound()..impl = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _delete(TextRange range) {
|
mojom.EditingState get editingState {
|
||||||
if (range.isCollapsed || !range.isValid) return;
|
return new mojom.EditingState()
|
||||||
text = range.textBefore(text) + range.textAfter(text);
|
..text = text
|
||||||
|
..selectionBase = selection.baseOffset
|
||||||
|
..selectionExtent = selection.extentOffset
|
||||||
|
..selectionAffinity = mojom.TextAffinity.values[selection.affinity.index]
|
||||||
|
..selectionIsDirectional = selection.isDirectional
|
||||||
|
..composingBase = composing.start
|
||||||
|
..composingExtent = composing.end;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextRange _append(String newText) {
|
void updateEditingState(mojom.EditingState state) {
|
||||||
int start = text.length;
|
text = state.text;
|
||||||
text += newText;
|
selection = _getTextSelectionFromEditingState(state);
|
||||||
return new TextRange(start: start, end: start + newText.length);
|
composing = new TextRange(start: state.composingBase, end: state.composingExtent);
|
||||||
}
|
|
||||||
|
|
||||||
TextRange _replace(TextRange range, String newText) {
|
|
||||||
assert(range.isValid);
|
|
||||||
|
|
||||||
String before = range.textBefore(text);
|
|
||||||
String after = range.textAfter(text);
|
|
||||||
|
|
||||||
text = before + newText + after;
|
|
||||||
return new TextRange(
|
|
||||||
start: before.length, end: before.length + newText.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
TextRange _replaceOrAppend(TextRange range, String newText) {
|
|
||||||
if (!range.isValid) return _append(newText);
|
|
||||||
return _replace(range, newText);
|
|
||||||
}
|
|
||||||
|
|
||||||
void commitCompletion(CompletionData completion) {
|
|
||||||
// TODO(abarth): Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
void commitCorrection(CorrectionData correction) {
|
|
||||||
// TODO(abarth): Not implemented.
|
|
||||||
}
|
|
||||||
|
|
||||||
void commitText(String text, int newCursorPosition) {
|
|
||||||
// TODO(abarth): Why is |newCursorPosition| always 1?
|
|
||||||
TextRange committedRange = _replaceOrAppend(composing, text);
|
|
||||||
selection = new TextSelection.collapsed(offset: committedRange.end);
|
|
||||||
composing = TextRange.empty;
|
|
||||||
onUpdated();
|
onUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteSurroundingText(int beforeLength, int afterLength) {
|
void submit(mojom.SubmitAction action) {
|
||||||
TextRange beforeRange = new TextRange(
|
|
||||||
start: selection.start - beforeLength, end: selection.start);
|
|
||||||
int afterRangeEnd = math.min(selection.end + afterLength, text.length);
|
|
||||||
TextRange afterRange =
|
|
||||||
new TextRange(start: selection.end, end: afterRangeEnd);
|
|
||||||
_delete(afterRange);
|
|
||||||
_delete(beforeRange);
|
|
||||||
selection = new TextSelection(
|
|
||||||
baseOffset: math.max(selection.start - beforeLength, 0),
|
|
||||||
extentOffset: math.max(selection.end - beforeLength, 0)
|
|
||||||
);
|
|
||||||
onUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setComposingRegion(int start, int end) {
|
|
||||||
composing = new TextRange(start: start, end: end);
|
|
||||||
onUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setComposingText(String text, int newCursorPosition) {
|
|
||||||
// TODO(abarth): Why is |newCursorPosition| always 1?
|
|
||||||
composing = _replaceOrAppend(composing, text);
|
|
||||||
selection = new TextSelection.collapsed(offset: composing.end);
|
|
||||||
onUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
void setSelection(int start, int end) {
|
|
||||||
selection = new TextSelection(baseOffset: start, extentOffset: end);
|
|
||||||
onUpdated();
|
|
||||||
}
|
|
||||||
|
|
||||||
void submit(SubmitAction action) {
|
|
||||||
composing = TextRange.empty;
|
composing = TextRange.empty;
|
||||||
onSubmitted();
|
onSubmitted();
|
||||||
}
|
}
|
||||||
@ -162,10 +113,12 @@ class EditableString {
|
|||||||
_client.selection = selection;
|
_client.selection = selection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mojom.EditingState get editingState => _client.editingState;
|
||||||
|
|
||||||
/// A keyboard client stub that can be attached to a keyboard service.
|
/// A keyboard client stub that can be attached to a keyboard service.
|
||||||
///
|
///
|
||||||
/// See [Keyboard].
|
/// See [Keyboard].
|
||||||
KeyboardClientStub createStub() => _client.createStub();
|
mojom.KeyboardClientStub createStub() => _client.createStub();
|
||||||
|
|
||||||
void didDetachKeyboard() {
|
void didDetachKeyboard() {
|
||||||
_client.composing = TextRange.empty;
|
_client.composing = TextRange.empty;
|
||||||
|
@ -5,31 +5,28 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
|
||||||
import 'package:mojo_services/keyboard/keyboard.mojom.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../services/mock_services.dart';
|
import '../services/mock_services.dart';
|
||||||
|
|
||||||
class MockKeyboard implements KeyboardService {
|
class MockKeyboard implements mojom.Keyboard {
|
||||||
KeyboardClient client;
|
mojom.KeyboardClient client;
|
||||||
|
|
||||||
void show(KeyboardClientStub client, KeyboardType type) {
|
void setClient(mojom.KeyboardClientStub client, mojom.KeyboardConfiguration configuraiton) {
|
||||||
this.client = client.impl;
|
this.client = client.impl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void showByRequest() {}
|
void show() {}
|
||||||
|
|
||||||
void hide() {}
|
void hide() {}
|
||||||
|
|
||||||
void setText(String text) {}
|
void setEditingState(mojom.EditingState state) {}
|
||||||
|
|
||||||
void setSelection(int start, int end) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
MockKeyboard mockKeyboard = new MockKeyboard();
|
MockKeyboard mockKeyboard = new MockKeyboard();
|
||||||
serviceMocker.registerMockService(KeyboardService.serviceName, mockKeyboard);
|
serviceMocker.registerMockService(mojom.Keyboard.serviceName, mockKeyboard);
|
||||||
|
|
||||||
test('Editable text has consistent size', () {
|
test('Editable text has consistent size', () {
|
||||||
testWidgets((WidgetTester tester) {
|
testWidgets((WidgetTester tester) {
|
||||||
@ -56,7 +53,10 @@ void main() {
|
|||||||
void enterText(String testValue) {
|
void enterText(String testValue) {
|
||||||
// Simulate entry of text through the keyboard.
|
// Simulate entry of text through the keyboard.
|
||||||
expect(mockKeyboard.client, isNotNull);
|
expect(mockKeyboard.client, isNotNull);
|
||||||
mockKeyboard.client.setComposingText(testValue, testValue.length);
|
mockKeyboard.client.updateEditingState(new mojom.EditingState()
|
||||||
|
..text = testValue
|
||||||
|
..composingBase = 0
|
||||||
|
..composingExtent = testValue.length);
|
||||||
|
|
||||||
// Check that the onChanged event handler fired.
|
// Check that the onChanged event handler fired.
|
||||||
expect(inputValue, equals(testValue));
|
expect(inputValue, equals(testValue));
|
||||||
@ -109,46 +109,14 @@ void main() {
|
|||||||
checkCursorToggle();
|
checkCursorToggle();
|
||||||
|
|
||||||
// Try the test again with a nonempty EditableText.
|
// Try the test again with a nonempty EditableText.
|
||||||
mockKeyboard.client.setComposingText('X', 1);
|
mockKeyboard.client.updateEditingState(new mojom.EditingState()
|
||||||
|
..text = 'X'
|
||||||
|
..selectionBase = 1
|
||||||
|
..selectionExtent = 1);
|
||||||
checkCursorToggle();
|
checkCursorToggle();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Selection remains valid', () {
|
|
||||||
testWidgets((WidgetTester tester) {
|
|
||||||
GlobalKey inputKey = new GlobalKey();
|
|
||||||
|
|
||||||
Widget builder() {
|
|
||||||
return new Center(
|
|
||||||
child: new Material(
|
|
||||||
child: new Input(
|
|
||||||
key: inputKey,
|
|
||||||
hintText: 'Placeholder'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
tester.pumpWidget(builder());
|
|
||||||
|
|
||||||
const String testValue = 'ABC';
|
|
||||||
mockKeyboard.client.commitText(testValue, testValue.length);
|
|
||||||
dynamic input = inputKey.currentState;
|
|
||||||
|
|
||||||
// Delete characters and verify that the selection follows the length
|
|
||||||
// of the text.
|
|
||||||
for (int i = 0; i < testValue.length; i++) {
|
|
||||||
mockKeyboard.client.deleteSurroundingText(1, 0);
|
|
||||||
expect(input.editableValue.selection.start, equals(testValue.length - i - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a characters when the text is empty. The selection should
|
|
||||||
// remain at zero.
|
|
||||||
mockKeyboard.client.deleteSurroundingText(1, 0);
|
|
||||||
expect(input.editableValue.selection.start, equals(0));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('hideText control test', () {
|
test('hideText control test', () {
|
||||||
testWidgets((WidgetTester tester) {
|
testWidgets((WidgetTester tester) {
|
||||||
GlobalKey inputKey = new GlobalKey();
|
GlobalKey inputKey = new GlobalKey();
|
||||||
@ -168,7 +136,10 @@ void main() {
|
|||||||
tester.pumpWidget(builder());
|
tester.pumpWidget(builder());
|
||||||
|
|
||||||
const String testValue = 'ABC';
|
const String testValue = 'ABC';
|
||||||
mockKeyboard.client.commitText(testValue, testValue.length);
|
mockKeyboard.client.updateEditingState(new mojom.EditingState()
|
||||||
|
..text = testValue
|
||||||
|
..selectionBase = testValue.length
|
||||||
|
..selectionExtent = testValue.length);
|
||||||
|
|
||||||
tester.pump();
|
tester.pump();
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user