Commit 012c5d94 authored by Ian Hickson's avatar Ian Hickson

Merge pull request #1496 from Hixie/focus

Fix Focus
parents 5a359dce fbf8174c
...@@ -53,14 +53,12 @@ class AddressBookHome extends StatelessComponent { ...@@ -53,14 +53,12 @@ class AddressBookHome extends StatelessComponent {
); );
} }
static final GlobalKey nameKey = new GlobalKey(); static final GlobalKey nameKey = new GlobalKey(label: 'name field');
static final GlobalKey phoneKey = new GlobalKey(); static final GlobalKey phoneKey = new GlobalKey(label: 'phone field');
static final GlobalKey emailKey = new GlobalKey(); static final GlobalKey emailKey = new GlobalKey(label: 'email field');
static final GlobalKey addressKey = new GlobalKey(); static final GlobalKey addressKey = new GlobalKey(label: 'address field');
static final GlobalKey ringtoneKey = new GlobalKey(); static final GlobalKey ringtoneKey = new GlobalKey(label: 'ringtone field');
static final GlobalKey noteKey = new GlobalKey(); static final GlobalKey noteKey = new GlobalKey(label: 'note field');
static final GlobalKey fillKey = new GlobalKey();
static final GlobalKey emoticonKey = new GlobalKey();
Widget buildBody(BuildContext context) { Widget buildBody(BuildContext context) {
return new Material( return new Material(
......
...@@ -41,7 +41,7 @@ class WidgetFlutterBinding extends FlutterBinding { ...@@ -41,7 +41,7 @@ class WidgetFlutterBinding extends FlutterBinding {
Element.finalizeTree(); Element.finalizeTree();
} }
List<BuildableElement> _dirtyElements = new List<BuildableElement>(); List<BuildableElement> _dirtyElements = <BuildableElement>[];
/// Adds an element to the dirty elements list so that it will be rebuilt /// Adds an element to the dirty elements list so that it will be rebuilt
/// when buildDirtyElements is called. /// when buildDirtyElements is called.
...@@ -62,10 +62,19 @@ class WidgetFlutterBinding extends FlutterBinding { ...@@ -62,10 +62,19 @@ class WidgetFlutterBinding extends FlutterBinding {
return; return;
BuildableElement.lockState(() { BuildableElement.lockState(() {
_dirtyElements.sort((BuildableElement a, BuildableElement b) => a.depth - b.depth); _dirtyElements.sort((BuildableElement a, BuildableElement b) => a.depth - b.depth);
for (BuildableElement element in _dirtyElements) int dirtyCount = _dirtyElements.length;
element.rebuild(); int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
index += 1;
if (dirtyCount < _dirtyElements.length) {
_dirtyElements.sort((BuildableElement a, BuildableElement b) => a.depth - b.depth);
dirtyCount = _dirtyElements.length;
}
}
assert(!_dirtyElements.any((BuildableElement element) => element.dirty));
_dirtyElements.clear(); _dirtyElements.clear();
}); }, building: true);
assert(_dirtyElements.isEmpty); assert(_dirtyElements.isEmpty);
} }
} }
...@@ -76,7 +85,7 @@ void runApp(Widget app) { ...@@ -76,7 +85,7 @@ void runApp(Widget app) {
WidgetFlutterBinding.instance.renderViewElement.update( WidgetFlutterBinding.instance.renderViewElement.update(
WidgetFlutterBinding.instance.describeApp(app) WidgetFlutterBinding.instance.describeApp(app)
); );
}); }, building: true);
} }
void debugDumpApp() { void debugDumpApp() {
......
...@@ -75,6 +75,52 @@ class Focus extends StatefulComponent { ...@@ -75,6 +75,52 @@ class Focus extends StatefulComponent {
final bool autofocus; final bool autofocus;
final Widget child; final Widget child;
static bool at(BuildContext context, Widget widget, { bool autofocus: true }) {
assert(widget != null);
assert(widget.key is GlobalKey);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedWidgetIfUnset(widget.key);
return focusScope.scopeFocused &&
focusScope.focusedScope == null &&
focusScope.focusedWidget == widget.key;
}
return true;
}
static bool _atScope(BuildContext context, Widget widget, { bool autofocus: true }) {
assert(widget != null);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedScopeIfUnset(widget.key);
assert(widget.key != null);
return focusScope.scopeFocused &&
focusScope.focusedScope == widget.key;
}
return true;
}
// Don't call moveTo() from your build() function, it's intended to be called
// from event listeners, e.g. in response to a finger tap or tab key.
static void moveTo(BuildContext context, Widget widget) {
assert(widget != null);
assert(widget.key is GlobalKey);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null)
focusScope.focusState._setFocusedWidget(widget.key);
}
static void _moveScopeTo(BuildContext context, Focus component) {
assert(component != null);
assert(component.key != null);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null)
focusScope.focusState._setFocusedScope(component.key);
}
FocusState createState() => new FocusState(); FocusState createState() => new FocusState();
} }
...@@ -155,7 +201,7 @@ class FocusState extends State<Focus> { ...@@ -155,7 +201,7 @@ class FocusState extends State<Focus> {
void initState() { void initState() {
super.initState(); super.initState();
if (config.autofocus) if (config.autofocus)
FocusState._moveScopeTo(context, config); Focus._moveScopeTo(context, config);
_updateWidgetRemovalListener(_focusedWidget); _updateWidgetRemovalListener(_focusedWidget);
_updateScopeRemovalListener(_focusedScope); _updateScopeRemovalListener(_focusedScope);
} }
...@@ -169,56 +215,10 @@ class FocusState extends State<Focus> { ...@@ -169,56 +215,10 @@ class FocusState extends State<Focus> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new _FocusScope( return new _FocusScope(
focusState: this, focusState: this,
scopeFocused: FocusState._atScope(context, config), scopeFocused: Focus._atScope(context, config),
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope, focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
focusedWidget: _focusedWidget, focusedWidget: _focusedWidget,
child: config.child child: config.child
); );
} }
static bool at(BuildContext context, Widget widget, { bool autofocus: true }) {
assert(widget != null);
assert(widget.key is GlobalKey);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedWidgetIfUnset(widget.key);
return focusScope.scopeFocused &&
focusScope.focusedScope == null &&
focusScope.focusedWidget == widget.key;
}
return true;
}
static bool _atScope(BuildContext context, Widget widget, { bool autofocus: true }) {
assert(widget != null);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedScopeIfUnset(widget.key);
assert(widget.key != null);
return focusScope.scopeFocused &&
focusScope.focusedScope == widget.key;
}
return true;
}
// Don't call moveTo() from your build() function, it's intended to be called
// from event listeners, e.g. in response to a finger tap or tab key.
static void moveTo(BuildContext context, Widget widget) {
assert(widget != null);
assert(widget.key is GlobalKey);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null)
focusScope.focusState._setFocusedWidget(widget.key);
}
static void _moveScopeTo(BuildContext context, Focus component) {
assert(component != null);
assert(component.key != null);
_FocusScope focusScope = context.inheritedWidgetOfType(_FocusScope);
if (focusScope != null)
focusScope.focusState._setFocusedScope(component.key);
}
} }
...@@ -186,8 +186,8 @@ abstract class Widget { ...@@ -186,8 +186,8 @@ abstract class Widget {
final List<String> data = <String>[]; final List<String> data = <String>[];
debugFillDescription(data); debugFillDescription(data);
if (data.isEmpty) if (data.isEmpty)
return 'name'; return '$name';
return 'name(${data.join("; ")})'; return '$name(${data.join("; ")})';
} }
void debugFillDescription(List<String> description) { } void debugFillDescription(List<String> description) { }
...@@ -550,7 +550,7 @@ abstract class Element<T extends Widget> implements BuildContext { ...@@ -550,7 +550,7 @@ abstract class Element<T extends Widget> implements BuildContext {
/// Wrapper around visitChildren for BuildContext. /// Wrapper around visitChildren for BuildContext.
void visitChildElements(void visitor(Element element)) { void visitChildElements(void visitor(Element element)) {
// don't allow visitChildElements() during build, since children aren't necessarily built yet // don't allow visitChildElements() during build, since children aren't necessarily built yet
assert(BuildableElement._debugStateLockLevel == 0); assert(!BuildableElement._debugStateLocked);
visitChildren(visitor); visitChildren(visitor);
} }
...@@ -858,6 +858,8 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -858,6 +858,8 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
assert(_child != null); assert(_child != null);
} }
static BuildableElement _debugCurrentBuildTarget;
/// Reinvokes the build() method of the StatelessComponent object (for /// Reinvokes the build() method of the StatelessComponent object (for
/// stateless components) or the State object (for stateful components) and /// stateless components) or the State object (for stateful components) and
/// then updates the widget tree. /// then updates the widget tree.
...@@ -874,6 +876,12 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -874,6 +876,12 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(_debugStateLocked); assert(_debugStateLocked);
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true)); assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
BuildableElement debugPreviousBuildTarget;
assert(() {
debugPreviousBuildTarget = _debugCurrentBuildTarget;
_debugCurrentBuildTarget = this;
return true;
});
Widget built; Widget built;
try { try {
built = _builder(this); built = _builder(this);
...@@ -896,12 +904,19 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -896,12 +904,19 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
built = new ErrorWidget(); built = new ErrorWidget();
_child = updateChild(null, built, slot); _child = updateChild(null, built, slot);
} }
assert(() {
assert(_debugCurrentBuildTarget == this);
_debugCurrentBuildTarget = debugPreviousBuildTarget;
return true;
});
} }
static BuildScheduler scheduleBuildFor; static BuildScheduler scheduleBuildFor;
static int _debugStateLockLevel = 0; static int _debugStateLockLevel = 0;
static bool get _debugStateLocked => _debugStateLockLevel > 0; static bool get _debugStateLocked => _debugStateLockLevel > 0;
static bool _debugBuilding = false;
/// Establishes a scope in which component build functions can run. /// Establishes a scope in which component build functions can run.
/// ///
...@@ -913,13 +928,31 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -913,13 +928,31 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
/// After unwinding the last build scope on the stack, the framework verifies /// After unwinding the last build scope on the stack, the framework verifies
/// that each global key is used at most once and notifies listeners about /// that each global key is used at most once and notifies listeners about
/// changes to global keys. /// changes to global keys.
static void lockState(void callback()) { static void lockState(void callback(), { bool building: false }) {
_debugStateLockLevel += 1; assert(_debugStateLockLevel >= 0);
assert(() {
if (building) {
assert(!_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = true;
}
_debugStateLockLevel += 1;
return true;
});
try { try {
callback(); callback();
} finally { } finally {
_debugStateLockLevel -= 1; assert(() {
_debugStateLockLevel -= 1;
if (building) {
assert(_debugBuilding);
assert(_debugCurrentBuildTarget == null);
_debugBuilding = false;
}
return true;
});
} }
assert(_debugStateLockLevel >= 0);
} }
/// Marks the element as dirty and adds it to the global list of widgets to /// Marks the element as dirty and adds it to the global list of widgets to
...@@ -934,10 +967,23 @@ abstract class BuildableElement<T extends Widget> extends Element<T> { ...@@ -934,10 +967,23 @@ abstract class BuildableElement<T extends Widget> extends Element<T> {
if (!_active) if (!_active)
return; return;
assert(_debugLifecycleState == _ElementLifecycle.active); assert(_debugLifecycleState == _ElementLifecycle.active);
assert(!_debugStateLocked || (_debugAllowIgnoredCallsToMarkNeedsBuild && dirty)); assert(() {
if (_debugBuilding) {
bool foundTarget = false;
visitAncestorElements((Element element) {
if (element == _debugCurrentBuildTarget) {
foundTarget = true;
return false;
}
return true;
});
if (foundTarget)
return true;
}
return !_debugStateLocked || (_debugAllowIgnoredCallsToMarkNeedsBuild && dirty);
});
if (dirty) if (dirty)
return; return;
assert(!_debugStateLocked);
_dirty = true; _dirty = true;
assert(scheduleBuildFor != null); assert(scheduleBuildFor != null);
scheduleBuildFor(this); scheduleBuildFor(this);
...@@ -1101,11 +1147,12 @@ class InheritedElement extends StatelessComponentElement<InheritedWidget> { ...@@ -1101,11 +1147,12 @@ class InheritedElement extends StatelessComponentElement<InheritedWidget> {
} }
void _notifyDescendants() { void _notifyDescendants() {
final Type ourRuntimeType = runtimeType; final Type ourRuntimeType = widget.runtimeType;
void notifyChildren(Element child) { void notifyChildren(Element child) {
if (child._dependencies != null && if (child._dependencies != null &&
child._dependencies.contains(ourRuntimeType)) child._dependencies.contains(ourRuntimeType)) {
child.dependenciesChanged(); child.dependenciesChanged();
}
if (child.runtimeType != ourRuntimeType) if (child.runtimeType != ourRuntimeType)
child.visitChildren(notifyChildren); child.visitChildren(notifyChildren);
} }
......
...@@ -71,7 +71,7 @@ class InputState extends ScrollableState<Input> { ...@@ -71,7 +71,7 @@ class InputState extends ScrollableState<Input> {
Widget buildContent(BuildContext context) { Widget buildContent(BuildContext context) {
ThemeData themeData = Theme.of(context); ThemeData themeData = Theme.of(context);
bool focused = FocusState.at(context, config); bool focused = Focus.at(context, config);
if (focused && !_keyboardHandle.attached) { if (focused && !_keyboardHandle.attached) {
_keyboardHandle = keyboard.show(_editableValue.stub, config.keyboardType); _keyboardHandle = keyboard.show(_editableValue.stub, config.keyboardType);
...@@ -122,11 +122,11 @@ class InputState extends ScrollableState<Input> { ...@@ -122,11 +122,11 @@ class InputState extends ScrollableState<Input> {
) )
), ),
onPointerDown: (_) { onPointerDown: (_) {
if (FocusState.at(context, config)) { if (Focus.at(context, config)) {
assert(_keyboardHandle.attached); assert(_keyboardHandle.attached);
_keyboardHandle.showByRequest(); _keyboardHandle.showByRequest();
} else { } else {
FocusState.moveTo(context, config); Focus.moveTo(context, config);
// 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
} }
} }
......
...@@ -66,6 +66,10 @@ class BadDisposeWidgetState extends State<BadDisposeWidget> { ...@@ -66,6 +66,10 @@ class BadDisposeWidgetState extends State<BadDisposeWidget> {
void main() { void main() {
dynamic cachedException; dynamic cachedException;
// ** WARNING **
// THIS TEST OVERRIDES THE NORMAL EXCEPTION HANDLING
// AND DOES NOT REPORT EXCEPTIONS FROM THE FRAMEWORK
setUp(() { setUp(() {
assert(cachedException == null); assert(cachedException == null);
debugWidgetsExceptionHandler = (String context, dynamic exception, StackTrace stack) { debugWidgetsExceptionHandler = (String context, dynamic exception, StackTrace stack) {
......
import 'package:sky/widgets.dart';
import 'package:test/test.dart';
import 'widget_tester.dart';
class TestFocusable extends StatelessComponent {
TestFocusable(this.no, this.yes, GlobalKey key) : super(key: key);
final String no;
final String yes;
Widget build(BuildContext context) {
bool focused = Focus.at(context, this);
return new GestureDetector(
onTap: () { Focus.moveTo(context, this); },
child: new Text(focused ? yes : no)
);
}
}
void main() {
test('Can have multiple focused children and they update accordingly', () {
testWidgets((WidgetTester tester) {
GlobalKey keyA = new GlobalKey();
GlobalKey keyB = new GlobalKey();
tester.pumpWidget(
new Focus(
child: new Column([
// reverse these when you fix https://github.com/flutter/engine/issues/1495
new TestFocusable('b', 'B FOCUSED', keyB),
new TestFocusable('a', 'A FOCUSED', keyA),
])
)
);
expect(tester.findText('a'), isNull);
expect(tester.findText('A FOCUSED'), isNotNull);
expect(tester.findText('b'), isNotNull);
expect(tester.findText('B FOCUSED'), isNull);
tester.tap(tester.findText('A FOCUSED'));
tester.pump();
expect(tester.findText('a'), isNull);
expect(tester.findText('A FOCUSED'), isNotNull);
expect(tester.findText('b'), isNotNull);
expect(tester.findText('B FOCUSED'), isNull);
tester.tap(tester.findText('A FOCUSED'));
tester.pump();
expect(tester.findText('a'), isNull);
expect(tester.findText('A FOCUSED'), isNotNull);
expect(tester.findText('b'), isNotNull);
expect(tester.findText('B FOCUSED'), isNull);
tester.tap(tester.findText('b'));
tester.pump();
expect(tester.findText('a'), isNotNull);
expect(tester.findText('A FOCUSED'), isNull);
expect(tester.findText('b'), isNull);
expect(tester.findText('B FOCUSED'), isNotNull);
tester.tap(tester.findText('a'));
tester.pump();
expect(tester.findText('a'), isNull);
expect(tester.findText('A FOCUSED'), isNotNull);
expect(tester.findText('b'), isNotNull);
expect(tester.findText('B FOCUSED'), isNull);
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment