focus.dart 8.44 KB
Newer Older
Eric Seidel's avatar
Eric Seidel committed
1 2 3 4
// 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.

5
import 'framework.dart';
Eric Seidel's avatar
Eric Seidel committed
6

7 8 9 10 11
// _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();
Eric Seidel's avatar
Eric Seidel committed
12

13
class _FocusScope extends InheritedWidget {
14 15
  _FocusScope({
    Key key,
16
    this.focusState,
17 18 19
    this.scopeFocused: true, // are we focused in our ancestor scope?
    this.focusedScope, // which of our descendant scopes is focused, if any?
    this.focusedWidget,
Eric Seidel's avatar
Eric Seidel committed
20 21 22
    Widget child
  }) : super(key: key, child: child);

23
  final FocusState focusState;
Hixie's avatar
Hixie committed
24
  final bool scopeFocused;
25

26
  // These are mutable because we implicitly change them when they're null in
27 28 29 30 31 32 33 34 35 36
  // 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) {
37 38 39
    focusState._setFocusedWidgetIfUnset(key);
    focusedWidget = focusState._focusedWidget;
    focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope;
40 41 42
  }

  void _setFocusedScopeIfUnset(GlobalKey key) {
43 44 45
    focusState._setFocusedScopeIfUnset(key);
    assert(focusedWidget == focusState._focusedWidget);
    focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope;
46 47
  }

48 49
  bool updateShouldNotify(_FocusScope oldWidget) {
    if (scopeFocused != oldWidget.scopeFocused)
50 51 52
      return true;
    if (!scopeFocused)
      return false;
53
    if (focusedScope != oldWidget.focusedScope)
54 55 56
      return true;
    if (focusedScope != null)
      return false;
57
    if (focusedWidget != oldWidget.focusedWidget)
58 59 60 61
      return true;
    return false;
  }

Hixie's avatar
Hixie committed
62 63 64 65 66 67 68 69 70
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (scopeFocused)
      description.add('this scope has focus');
    if (focusedScope != null)
      description.add('focused subscope: $focusedScope');
    if (focusedWidget != null)
      description.add('focused widget: $focusedWidget');
  }
71 72 73 74 75 76 77 78 79 80 81
}

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);
  }

82 83
  final bool autofocus;
  final Widget child;
84

85 86 87 88 89 90 91
  static GlobalKey debugOnlyFocusedKey;

  static bool at(BuildContext context, { bool autofocus: true }) {
    assert(context != null);
    assert(context.widget != null);
    assert(context.widget.key != null);
    assert(context.widget.key is GlobalKey);
Hixie's avatar
Hixie committed
92
    _FocusScope focusScope = context.inheritFromWidgetOfType(_FocusScope);
Hixie's avatar
Hixie committed
93 94
    if (focusScope != null) {
      if (autofocus)
95
        focusScope._setFocusedWidgetIfUnset(context.widget.key);
Hixie's avatar
Hixie committed
96 97
      return focusScope.scopeFocused &&
             focusScope.focusedScope == null &&
98
             focusScope.focusedWidget == context.widget.key;
Hixie's avatar
Hixie committed
99
    }
100 101 102 103 104 105 106 107 108
    assert(() {
      if (debugOnlyFocusedKey?.currentContext == null)
        debugOnlyFocusedKey = context.widget.key;
      if (debugOnlyFocusedKey != context.widget.key) {
        debugPrint('Tried to focus widgets with two different keys: $debugOnlyFocusedKey and ${context.widget.key}');
        assert('If you have more than one focusable widget, then you should put them inside a Focus.' == true);
      }
      return true;
    });
Hixie's avatar
Hixie committed
109 110 111
    return true;
  }

112 113 114 115
  static bool _atScope(BuildContext context, { bool autofocus: true }) {
    assert(context != null);
    assert(context.widget != null);
    assert(context.widget is Focus);
Hixie's avatar
Hixie committed
116
    _FocusScope focusScope = context.inheritFromWidgetOfType(_FocusScope);
Hixie's avatar
Hixie committed
117
    if (focusScope != null) {
118
      assert(context.widget.key != null);
Hixie's avatar
Hixie committed
119
      if (autofocus)
120
        focusScope._setFocusedScopeIfUnset(context.widget.key);
Hixie's avatar
Hixie committed
121
      return focusScope.scopeFocused &&
122
             focusScope.focusedScope == context.widget.key;
Hixie's avatar
Hixie committed
123 124 125 126
    }
    return true;
  }

127 128 129 130 131 132 133 134
  /// Focuses a particular widget, identified by its GlobalKey.
  /// The widget must be in the widget tree.
  /// 
  /// Don't call moveTo() from your build() functions, it's intended to be
  /// called from event listeners, e.g. in response to a finger tap or tab key.
  static void moveTo(GlobalKey key) {
    assert(key.currentContext != null);
    _FocusScope focusScope = key.currentContext.ancestorWidgetOfType(_FocusScope);
Hixie's avatar
Hixie committed
135
    if (focusScope != null)
136 137 138 139 140 141 142 143 144 145 146 147
      focusScope.focusState._setFocusedWidget(key);
  }

  /// Focuses a particular focus scope, identified by its GlobalKey. The widget
  /// must be in the widget tree.
  /// 
  /// Don't call moveScopeTo() from your build() functions, it's intended to be
  /// called from event listeners, e.g. in response to a finger tap or tab key.
  static void moveScopeTo(GlobalKey key) {
    assert(key.currentWidget is Focus);
    assert(key.currentContext != null);
    _FocusScope focusScope = key.currentContext.ancestorWidgetOfType(_FocusScope);
Hixie's avatar
Hixie committed
148
    if (focusScope != null)
149
      focusScope.focusState._setFocusedScope(key);
Hixie's avatar
Hixie committed
150 151
  }

152 153
  FocusState createState() => new FocusState();
}
154

155
class FocusState extends State<Focus> {
156 157
  GlobalKey _focusedWidget; // when null, the first component to ask if it's focused will get the focus
  GlobalKey _currentlyRegisteredWidgetRemovalListenerKey;
Eric Seidel's avatar
Eric Seidel committed
158

159 160 161 162 163 164 165
  void _setFocusedWidget(GlobalKey key) {
    setState(() {
      _focusedWidget = key;
      if (_focusedScope == null)
        _focusedScope = _noFocusedScope;
    });
    _updateWidgetRemovalListener(key);
Eric Seidel's avatar
Eric Seidel committed
166
  }
167 168 169 170 171 172

  void _setFocusedWidgetIfUnset(GlobalKey key) {
    if (_focusedWidget == null && (_focusedScope == null || _focusedScope == _noFocusedScope)) {
      _focusedWidget = key;
      _focusedScope = _noFocusedScope;
      _updateWidgetRemovalListener(key);
Eric Seidel's avatar
Eric Seidel committed
173 174 175
    }
  }

176
  void _handleWidgetRemoved(GlobalKey key) {
177
    assert(_focusedWidget == key);
178
    _updateWidgetRemovalListener(null);
179 180 181 182 183 184 185 186
    setState(() {
      _focusedWidget = null;
    });
  }

  void _updateWidgetRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredWidgetRemovalListenerKey != key) {
      if (_currentlyRegisteredWidgetRemovalListenerKey != null)
187
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved);
188
      if (key != null)
189
        GlobalKey.registerRemoveListener(key, _handleWidgetRemoved);
190 191 192 193 194 195 196 197 198 199 200 201 202
      _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);
  }
Eric Seidel's avatar
Eric Seidel committed
203

204 205 206 207 208 209 210 211 212
  void _setFocusedScopeIfUnset(GlobalKey key) {
    if (_focusedScope == null) {
      _focusedScope = key;
      _updateScopeRemovalListener(key);
    }
  }

  void _scopeRemoved(GlobalKey key) {
    assert(_focusedScope == key);
213
    GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
214 215 216 217 218 219 220 221 222
    _currentlyRegisteredScopeRemovalListenerKey = null;
    setState(() {
      _focusedScope = null;
    });
  }

  void _updateScopeRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredScopeRemovalListenerKey != key) {
      if (_currentlyRegisteredScopeRemovalListenerKey != null)
223
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
224
      if (key != null)
225
        GlobalKey.registerRemoveListener(key, _scopeRemoved);
226 227 228 229
      _currentlyRegisteredScopeRemovalListenerKey = key;
    }
  }

230 231
  void initState() {
    super.initState();
232 233 234 235
    _updateWidgetRemovalListener(_focusedWidget);
    _updateScopeRemovalListener(_focusedScope);
  }

236
  void dispose() {
237 238
    _updateWidgetRemovalListener(null);
    _updateScopeRemovalListener(null);
239
    super.dispose();
240 241
  }

242
  Widget build(BuildContext context) {
243
    return new _FocusScope(
244
      focusState: this,
245
      scopeFocused: Focus._atScope(context),
246 247
      focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
      focusedWidget: _focusedWidget,
248
      child: config.child
249 250
    );
  }
Eric Seidel's avatar
Eric Seidel committed
251
}