blob: 44f8aaacfa52e7b605457062d89f23d3fdeb5280 [file] [log] [blame]
// 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/widgets/framework.dart';
typedef void FocusChanged(GlobalKey key);
// _noFocusedScope is used by Focus to track the case where none of the Focus
// component's subscopes (e.g. dialogs) are focused. This is distinct from the
// focused scope being null, which means that we haven't yet decided which scope
// is focused and whichever is the first scope to ask for focus will get it.
final GlobalKey _noFocusedScope = new GlobalKey();
class _FocusScope extends Inherited {
_FocusScope({
Key key,
this.scopeFocused: true, // are we focused in our ancestor scope?
this.focusedScope, // which of our descendant scopes is focused, if any?
this.focusedWidget,
Widget child
}) : super(key: key, child: child);
final bool scopeFocused;
// These are mutable because we implicitly changed them when they're null in
// certain cases, basically pretending retroactively that we were constructed
// with the right keys.
GlobalKey focusedScope;
GlobalKey focusedWidget;
// The ...IfUnset() methods don't need to notify descendants because by
// definition they are only going to make a change the very first time that
// our state is checked.
void _setFocusedWidgetIfUnset(GlobalKey key) {
assert(parent is Focus);
(parent as Focus)._setFocusedWidgetIfUnset(key); // TODO(ianh): remove cast once analyzer is cleverer
focusedWidget = (parent as Focus)._focusedWidget;
focusedScope = (parent as Focus)._focusedScope == _noFocusedScope ? null : (parent as Focus)._focusedScope;
}
void _setFocusedScopeIfUnset(GlobalKey key) {
assert(parent is Focus);
(parent as Focus)._setFocusedScopeIfUnset(key); // TODO(ianh): remove cast once analyzer is cleverer
assert(focusedWidget == (parent as Focus)._focusedWidget);
focusedScope = (parent as Focus)._focusedScope == _noFocusedScope ? null : (parent as Focus)._focusedScope;
}
bool syncShouldNotify(_FocusScope old) {
assert(parent is Focus);
if (scopeFocused != old.scopeFocused)
return true;
if (!scopeFocused)
return false;
if (focusedScope != old.focusedScope)
return true;
if (focusedScope != null)
return false;
if (focusedWidget != old.focusedWidget)
return true;
return false;
}
}
class Focus extends StatefulComponent {
Focus({
GlobalKey key, // key is required if this is a nested Focus scope
this.autofocus: false,
this.child
}) : super(key: key) {
assert(!autofocus || key != null);
}
bool autofocus;
Widget child;
void syncConstructorArguments(Focus source) {
autofocus = source.autofocus;
child = source.child;
}
GlobalKey _focusedWidget; // when null, the first component to ask if it's focused will get the focus
GlobalKey _currentlyRegisteredWidgetRemovalListenerKey;
void _setFocusedWidget(GlobalKey key) {
setState(() {
_focusedWidget = key;
if (_focusedScope == null)
_focusedScope = _noFocusedScope;
});
_updateWidgetRemovalListener(key);
}
void _setFocusedWidgetIfUnset(GlobalKey key) {
if (_focusedWidget == null && (_focusedScope == null || _focusedScope == _noFocusedScope)) {
_focusedWidget = key;
_focusedScope = _noFocusedScope;
_updateWidgetRemovalListener(key);
}
}
void _handleWidgetRemoved(GlobalKey key) {
assert(_focusedWidget == key);
_updateWidgetRemovalListener(null);
setState(() {
_focusedWidget = null;
});
}
void _updateWidgetRemovalListener(GlobalKey key) {
if (_currentlyRegisteredWidgetRemovalListenerKey != key) {
if (_currentlyRegisteredWidgetRemovalListenerKey != null)
GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved);
if (key != null)
GlobalKey.registerRemoveListener(key, _handleWidgetRemoved);
_currentlyRegisteredWidgetRemovalListenerKey = key;
}
}
GlobalKey _focusedScope; // when null, the first scope to ask if it's focused will get the focus
GlobalKey _currentlyRegisteredScopeRemovalListenerKey;
void _setFocusedScope(GlobalKey key) {
setState(() {
_focusedScope = key;
});
_updateScopeRemovalListener(key);
}
void _setFocusedScopeIfUnset(GlobalKey key) {
if (_focusedScope == null) {
_focusedScope = key;
_updateScopeRemovalListener(key);
}
}
void _scopeRemoved(GlobalKey key) {
assert(_focusedScope == key);
_currentlyRegisteredScopeRemovalListenerKey = null;
setState(() {
_focusedScope = null;
});
}
void _updateScopeRemovalListener(GlobalKey key) {
if (_currentlyRegisteredScopeRemovalListenerKey != key) {
if (_currentlyRegisteredScopeRemovalListenerKey != null)
GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
if (key != null)
GlobalKey.registerRemoveListener(key, _scopeRemoved);
_currentlyRegisteredScopeRemovalListenerKey = key;
}
}
bool _didAutoFocus = false;
void didMount() {
if (autofocus && !_didAutoFocus) {
_didAutoFocus = true;
Focus._moveScopeTo(this);
}
_updateWidgetRemovalListener(_focusedWidget);
_updateScopeRemovalListener(_focusedScope);
super.didMount();
}
void didUnmount() {
_updateWidgetRemovalListener(null);
_updateScopeRemovalListener(null);
super.didUnmount();
}
Widget build() {
return new _FocusScope(
scopeFocused: Focus._atScope(this),
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
focusedWidget: _focusedWidget,
child: child
);
}
static bool at(Component component, { bool autofocus: true }) {
assert(component != null);
assert(component.key is GlobalKey);
_FocusScope focusScope = component.inheritedOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedWidgetIfUnset(component.key);
return focusScope.scopeFocused &&
focusScope.focusedScope == null &&
focusScope.focusedWidget == component.key;
}
return true;
}
static bool _atScope(Focus component, { bool autofocus: true }) {
assert(component != null);
_FocusScope focusScope = component.inheritedOfType(_FocusScope);
if (focusScope != null) {
if (autofocus)
focusScope._setFocusedScopeIfUnset(component.key);
assert(component.key != null);
return focusScope.scopeFocused &&
focusScope.focusedScope == component.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(Component component) {
assert(component != null);
assert(component.key is GlobalKey);
_FocusScope focusScope = component.inheritedOfType(_FocusScope);
if (focusScope != null) {
assert(focusScope.parent is Focus);
(focusScope.parent as Focus)._setFocusedWidget(component.key); // TODO(ianh): remove cast once analyzer is cleverer
}
}
static void _moveScopeTo(Focus component) {
assert(component != null);
assert(component.key != null);
_FocusScope focusScope = component.inheritedOfType(_FocusScope);
if (focusScope != null) {
assert(focusScope.parent is Focus);
(focusScope.parent as Focus)._setFocusedScope(component.key); // TODO(ianh): remove cast once analyzer is cleverer
}
}
String toStringName() {
return '${super.toStringName()}(focusedScope=$_focusedScope; focusedWidget=$_focusedWidget)';
}
}