Make Sky's EditableText mostly work
This CL factors EditableString out of EditableText and implements more of the InputConnection functions. As a result, EditableText now basically works. This CL also paves the way to make EditableText stateless by using EditableString as its state object. However, there's still a bit more work to do to make that a reality (e.g., factoring out the cursor blink timer and the connection to the keyboard). R=eseidel@chromium.org Review URL: https://codereview.chromium.org/995073002
This commit is contained in:
parent
ae27b3ee0e
commit
b19dbcdea4
119
engine/src/flutter/examples/editor/editable_string.dart
Normal file
119
engine/src/flutter/examples/editor/editable_string.dart
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:sky/services/keyboard/keyboard.mojom.dart';
|
||||
|
||||
typedef void StringChangedCallback(EditableString updated);
|
||||
|
||||
class TextRange {
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
TextRange({this.start, this.end});
|
||||
TextRange.collapsed(int position) : start = position, end = position;
|
||||
const TextRange.empty() : start = -1, end = -1;
|
||||
|
||||
bool get isValid => start >= 0 && end >= 0;
|
||||
bool get isCollapsed => start == end;
|
||||
}
|
||||
|
||||
class EditableString implements KeyboardClient {
|
||||
String text;
|
||||
TextRange composing = const TextRange.empty();
|
||||
TextRange selection = const TextRange.empty();
|
||||
|
||||
final StringChangedCallback onChanged;
|
||||
|
||||
KeyboardClientStub stub;
|
||||
|
||||
EditableString({this.text: '', this.onChanged}) {
|
||||
stub = new KeyboardClientStub.unbound()..impl = this;
|
||||
}
|
||||
|
||||
String textBefore(TextRange range) {
|
||||
return text.substring(0, range.start);
|
||||
}
|
||||
|
||||
String textAfter(TextRange range) {
|
||||
return text.substring(range.end);
|
||||
}
|
||||
|
||||
String textInside(TextRange range) {
|
||||
return text.substring(range.start, range.end);
|
||||
}
|
||||
|
||||
void _delete(TextRange range) {
|
||||
if (range.isCollapsed || !range.isValid)
|
||||
return;
|
||||
text = textBefore(range) + textAfter(range);
|
||||
}
|
||||
|
||||
TextRange _append(String newText) {
|
||||
int start = text.length;
|
||||
text += newText;
|
||||
return new TextRange(start: start, end: start + newText.length);
|
||||
}
|
||||
|
||||
TextRange _replace(TextRange range, String newText) {
|
||||
assert(range.isValid);
|
||||
|
||||
String before = textBefore(range);
|
||||
String after = textAfter(range);
|
||||
|
||||
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 TextRange.collapsed(committedRange.end);
|
||||
composing = const TextRange.empty();
|
||||
onChanged(this);
|
||||
}
|
||||
|
||||
void deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
TextRange beforeRange = new TextRange(start: selection.start - beforeLength,
|
||||
end: selection.start);
|
||||
TextRange afterRange = new TextRange(start: selection.end,
|
||||
end: selection.end + afterLength);
|
||||
_delete(afterRange);
|
||||
_delete(beforeRange);
|
||||
selection = new TextRange(start: selection.start - beforeLength,
|
||||
end: selection.end - beforeLength);
|
||||
onChanged(this);
|
||||
}
|
||||
|
||||
void setComposingRegion(int start, int end) {
|
||||
composing = new TextRange(start: start, end: end);
|
||||
onChanged(this);
|
||||
}
|
||||
|
||||
void setComposingText(String text, int newCursorPosition) {
|
||||
// TODO(abarth): Why is |newCursorPosition| always 1?
|
||||
composing = _replaceOrAppend(composing, text);
|
||||
selection = new TextRange.collapsed(composing.end);
|
||||
onChanged(this);
|
||||
}
|
||||
|
||||
void setSelection(int start, int end) {
|
||||
selection = new TextRange(start: start, end: end);
|
||||
onChanged(this);
|
||||
}
|
||||
}
|
124
engine/src/flutter/examples/editor/editable_text.dart
Normal file
124
engine/src/flutter/examples/editor/editable_text.dart
Normal file
@ -0,0 +1,124 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../../framework/fn.dart';
|
||||
import '../../framework/shell.dart' as shell;
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'editable_string.dart';
|
||||
import 'package:sky/services/keyboard/keyboard.mojom.dart';
|
||||
|
||||
class EditableText extends Component {
|
||||
static Style _style = new Style('''
|
||||
display: paragraph;
|
||||
white-space: pre-wrap;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
background-color: lightblue;'''
|
||||
);
|
||||
|
||||
static Style _cusorStyle = new Style('''
|
||||
display: inline-block;
|
||||
width: 2px;
|
||||
height: 1.2em;
|
||||
vertical-align: top;
|
||||
background-color: blue;'''
|
||||
);
|
||||
|
||||
static Style _composingStyle = new Style('''
|
||||
display: inline;
|
||||
text-decoration: underline;'''
|
||||
);
|
||||
|
||||
KeyboardServiceProxy _service;
|
||||
|
||||
EditableString _string;
|
||||
Timer _cursorTimer;
|
||||
bool _showCursor = false;
|
||||
|
||||
EditableText({Object key}) : super(key: key, stateful: true) {
|
||||
_string = new EditableString(text: '', onChanged: _handleTextChanged);
|
||||
events.listen('click', _handleClick);
|
||||
events.listen('focus', _handleFocus);
|
||||
events.listen('blur', _handleBlur);
|
||||
}
|
||||
|
||||
void _handleTextChanged(EditableString string) {
|
||||
setState(() {
|
||||
_string = string;
|
||||
_showCursor = true;
|
||||
_restartCursorTimer();
|
||||
});
|
||||
}
|
||||
|
||||
void _handleClick(_) {
|
||||
if (_service != null)
|
||||
return;
|
||||
_service = new KeyboardServiceProxy.unbound();
|
||||
shell.requestService(_service);
|
||||
_service.ptr.show(_string.stub);
|
||||
_restartCursorTimer();
|
||||
setState(() {
|
||||
_showCursor = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleFocus(_) {
|
||||
print("_handleFocus");
|
||||
}
|
||||
|
||||
void _handleBlur(_) {
|
||||
print("_handleBlur");
|
||||
}
|
||||
|
||||
void _cursorTick(Timer timer) {
|
||||
setState(() {
|
||||
_showCursor = !_showCursor;
|
||||
});
|
||||
}
|
||||
|
||||
void _restartCursorTimer() {
|
||||
if (_cursorTimer != null)
|
||||
_cursorTimer.cancel();
|
||||
_cursorTimer = new Timer.periodic(
|
||||
new Duration(milliseconds: 500), _cursorTick);
|
||||
}
|
||||
|
||||
void didUnmount() {
|
||||
_cursorTimer.cancel();
|
||||
}
|
||||
|
||||
Node build() {
|
||||
List<Node> children = new List<Node>();
|
||||
|
||||
if (!_string.composing.isValid) {
|
||||
children.add(new Text(_string.text));
|
||||
} else {
|
||||
String beforeComposing = _string.textBefore(_string.composing);
|
||||
if (!beforeComposing.isEmpty)
|
||||
children.add(new Text(beforeComposing));
|
||||
|
||||
String composing = _string.textInside(_string.composing);
|
||||
if (!composing.isEmpty) {
|
||||
children.add(new Container(
|
||||
key: 'composing',
|
||||
style: _composingStyle,
|
||||
children: [new Text(composing)]
|
||||
));
|
||||
}
|
||||
|
||||
String afterComposing = _string.textAfter(_string.composing);
|
||||
if (!afterComposing.isEmpty)
|
||||
children.add(new Text(afterComposing));
|
||||
}
|
||||
|
||||
if (_showCursor)
|
||||
children.add(new Container(key: 'cursor', style: _cusorStyle));
|
||||
|
||||
return new Container(
|
||||
style: _style,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
}
|
@ -3,10 +3,10 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../../framework/fn.dart';
|
||||
import 'input.dart';
|
||||
import 'editable_text.dart';
|
||||
|
||||
class EditorApp extends App {
|
||||
Node build() {
|
||||
return new Input();
|
||||
return new EditableText();
|
||||
}
|
||||
}
|
||||
|
@ -1,111 +0,0 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../../framework/fn.dart';
|
||||
import '../../framework/shell.dart' as shell;
|
||||
import 'package:sky/services/keyboard/keyboard.mojom.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class Input extends Component implements KeyboardClient{
|
||||
static Style _style = new Style('''
|
||||
display: paragraph;
|
||||
padding: 10px;
|
||||
height: 200px;
|
||||
background-color: lightblue;'''
|
||||
);
|
||||
|
||||
static Style _composingStyle = new Style('''
|
||||
display: inline;
|
||||
text-decoration: underline;'''
|
||||
);
|
||||
|
||||
KeyboardServiceProxy _service;
|
||||
KeyboardClientStub _stub;
|
||||
|
||||
String _text = "";
|
||||
int _composingStart = -1;
|
||||
int _composingEnd = -1;
|
||||
|
||||
Input({Object key}) : super(key: key, stateful: true) {
|
||||
events.listen('click', _handleClick);
|
||||
_stub = new KeyboardClientStub.unbound()..impl = this;
|
||||
}
|
||||
|
||||
bool get _hasComposingRegion => _composingStart != -1 && _composingEnd != -1;
|
||||
|
||||
void _handleClick(_) {
|
||||
if (_service != null)
|
||||
return;
|
||||
_service = new KeyboardServiceProxy.unbound();
|
||||
shell.requestService(_service);
|
||||
_service.ptr.show(_stub);
|
||||
}
|
||||
|
||||
void _replaceComposing(String text) {
|
||||
if (!_hasComposingRegion) {
|
||||
_composingStart = _text.length;
|
||||
_composingEnd = _composingStart + text.length;
|
||||
_text += text;
|
||||
return;
|
||||
}
|
||||
|
||||
_text = _text.substring(0, _composingStart)
|
||||
+ text + _text.substring(_composingEnd);
|
||||
_composingEnd = _composingStart + text.length;
|
||||
}
|
||||
|
||||
void _clearComposingRegion() {
|
||||
_composingStart = -1;
|
||||
_composingEnd = -1;
|
||||
}
|
||||
|
||||
void commitText(String text, int newCursorPosition) {
|
||||
setState(() {
|
||||
_replaceComposing(text);
|
||||
_clearComposingRegion();
|
||||
});
|
||||
}
|
||||
|
||||
void setComposingText(String text, int newCursorPosition) {
|
||||
setState(() {
|
||||
_replaceComposing(text);
|
||||
});
|
||||
}
|
||||
|
||||
void setComposingRegion(int start, int end) {
|
||||
setState(() {
|
||||
_composingStart = start;
|
||||
_composingEnd = end;
|
||||
});
|
||||
}
|
||||
|
||||
Node build() {
|
||||
List<Node> children = new List<Node>();
|
||||
|
||||
if (!_hasComposingRegion) {
|
||||
children.add(new Text(_text));
|
||||
} else {
|
||||
String run = _text.substring(0, _composingStart);
|
||||
if (!run.isEmpty)
|
||||
children.add(new Text(run));
|
||||
|
||||
run = _text.substring(_composingStart, _composingEnd);
|
||||
if (!run.isEmpty) {
|
||||
children.add(new Container(
|
||||
style: _composingStyle,
|
||||
children: [new Text(_text.substring(_composingStart, _composingEnd))]
|
||||
));
|
||||
}
|
||||
|
||||
run = _text.substring(_composingEnd);
|
||||
if (!run.isEmpty)
|
||||
children.add(new Text(_text.substring(_composingEnd)));
|
||||
}
|
||||
|
||||
return new Container(
|
||||
style: _style,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user