
This lets clients listen for when a widget with a given global key goes through a sync operation. We'll need this for mimic to track its mimicable when it moves around the tree.
243 lines
7.6 KiB
Dart
243 lines
7.6 KiB
Dart
// 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)';
|
|
}
|
|
|
|
}
|