focus.dart 9.32 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 6 7
import 'dart:async';

import 'basic.dart';
8
import 'framework.dart';
9 10
import 'media_query.dart';
import 'scrollable.dart';
Eric Seidel's avatar
Eric Seidel committed
11

12 13 14 15 16
// _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
17

18
class _FocusScope extends InheritedWidget {
19 20
  _FocusScope({
    Key key,
21
    this.focusState,
22 23 24
    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
25 26 27
    Widget child
  }) : super(key: key, child: child);

28
  final _FocusState focusState;
Hixie's avatar
Hixie committed
29
  final bool scopeFocused;
30

31
  // These are mutable because we implicitly change them when they're null in
32 33 34 35 36
  // certain cases, basically pretending retroactively that we were constructed
  // with the right keys.
  GlobalKey focusedScope;
  GlobalKey focusedWidget;

37 38 39
  // The _setFocusedWidgetIfUnset() methodsdon'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.
40 41

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

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

Hixie's avatar
Hixie committed
61 62 63 64 65 66 67 68 69
  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');
  }
70 71 72 73
}

class Focus extends StatefulComponent {
  Focus({
74
    GlobalKey key,
75 76
    this.child
  }) : super(key: key) {
77
    assert(key != null);
78 79
  }

80
  final Widget child;
81

82 83
  static GlobalKey debugOnlyFocusedKey;

84
  static bool at(BuildContext context, { bool autofocus: false }) {
85 86 87 88
    assert(context != null);
    assert(context.widget != null);
    assert(context.widget.key != null);
    assert(context.widget.key is GlobalKey);
Ian Hickson's avatar
Ian Hickson committed
89
    _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
Hixie's avatar
Hixie committed
90 91
    if (focusScope != null) {
      if (autofocus)
92
        focusScope._setFocusedWidgetIfUnset(context.widget.key);
Hixie's avatar
Hixie committed
93 94
      return focusScope.scopeFocused &&
             focusScope.focusedScope == null &&
95
             focusScope.focusedWidget == context.widget.key;
Hixie's avatar
Hixie committed
96
    }
97 98 99 100 101 102 103 104 105
    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
106 107 108
    return true;
  }

109
  static bool _atScope(BuildContext context) {
110 111 112
    assert(context != null);
    assert(context.widget != null);
    assert(context.widget is Focus);
113
    assert(context.widget.key != null);
Ian Hickson's avatar
Ian Hickson committed
114
    _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
Hixie's avatar
Hixie committed
115 116
    if (focusScope != null) {
      return focusScope.scopeFocused &&
117
             focusScope.focusedScope == context.widget.key;
Hixie's avatar
Hixie committed
118 119 120 121
    }
    return true;
  }

122 123
  /// Focuses a particular widget, identified by its GlobalKey.
  /// The widget must be in the widget tree.
124
  ///
125 126 127
  /// 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) {
128 129
    BuildContext focusedContext = key.currentContext;
    assert(focusedContext != null);
Ian Hickson's avatar
Ian Hickson committed
130
    _FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
131
    if (focusScope != null) {
132
      focusScope.focusState._setFocusedWidget(key);
133 134
      Scrollable.ensureVisible(focusedContext);
    }
135 136
  }

137 138 139 140 141 142
  static void clear(BuildContext context) {
    _FocusScope focusScope = context.ancestorWidgetOfExactType(_FocusScope);
    if (focusScope != null)
      focusScope.focusState._clearFocusedWidget();
  }

143
  /// Focuses a particular focus scope, identified by its GlobalKey.
144
  ///
145 146
  /// 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.
147 148 149 150 151 152 153 154 155 156
  static void moveScopeTo(GlobalKey key, { BuildContext context }) {
    _FocusScope focusScope;
    BuildContext searchContext = key.currentContext;
    if (searchContext != null) {
      assert(key.currentWidget is Focus);
      focusScope = searchContext.ancestorWidgetOfExactType(_FocusScope);
      assert(context == null || focusScope == context.ancestorWidgetOfExactType(_FocusScope));
    } else {
      focusScope = context.ancestorWidgetOfExactType(_FocusScope);
    }
Hixie's avatar
Hixie committed
157
    if (focusScope != null)
158
      focusScope.focusState._setFocusedScope(key);
Hixie's avatar
Hixie committed
159 160
  }

161
  _FocusState createState() => new _FocusState();
162
}
163

164
class _FocusState extends State<Focus> {
165 166 167 168 169 170 171 172 173 174 175 176
  void initState() {
    super.initState();
    _updateWidgetRemovalListener(_focusedWidget);
    _updateScopeRemovalListener(_focusedScope);
  }

  void dispose() {
    _updateWidgetRemovalListener(null);
    _updateScopeRemovalListener(null);
    super.dispose();
  }

177 178
  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
179

180 181 182 183 184 185 186
  void _setFocusedWidget(GlobalKey key) {
    setState(() {
      _focusedWidget = key;
      if (_focusedScope == null)
        _focusedScope = _noFocusedScope;
    });
    _updateWidgetRemovalListener(key);
Eric Seidel's avatar
Eric Seidel committed
187
  }
188 189 190 191 192 193

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

197 198 199 200 201 202 203 204 205
  void _clearFocusedWidget() {
    if (_focusedWidget != null) {
      _updateWidgetRemovalListener(null);
      setState(() {
        _focusedWidget = null;
      });
    }
  }

206
  void _handleWidgetRemoved(GlobalKey key) {
207
    assert(key != null);
208
    assert(_focusedWidget == key);
209
    _clearFocusedWidget();
210 211 212 213 214
  }

  void _updateWidgetRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredWidgetRemovalListenerKey != key) {
      if (_currentlyRegisteredWidgetRemovalListenerKey != null)
215
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved);
216
      if (key != null)
217
        GlobalKey.registerRemoveListener(key, _handleWidgetRemoved);
218 219 220 221 222 223 224 225 226 227 228 229 230
      _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
231

232 233
  void _scopeRemoved(GlobalKey key) {
    assert(_focusedScope == key);
234
    GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
235 236 237 238 239 240 241 242 243
    _currentlyRegisteredScopeRemovalListenerKey = null;
    setState(() {
      _focusedScope = null;
    });
  }

  void _updateScopeRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredScopeRemovalListenerKey != key) {
      if (_currentlyRegisteredScopeRemovalListenerKey != null)
244
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
245
      if (key != null)
246
        GlobalKey.registerRemoveListener(key, _scopeRemoved);
247 248 249 250
      _currentlyRegisteredScopeRemovalListenerKey = key;
    }
  }

251 252 253 254 255 256 257 258 259 260 261 262
  Size _mediaSize;
  EdgeDims _mediaPadding;

  void _ensureVisibleIfFocused() {
    if (!Focus._atScope(context))
      return;
    BuildContext focusedContext = _focusedWidget?.currentContext;
    if (focusedContext == null)
      return;
    Scrollable.ensureVisible(focusedContext);
  }

263
  Widget build(BuildContext context) {
264 265 266 267 268 269 270 271 272 273
    MediaQueryData data = MediaQuery.of(context);
    if (data != null) {
      Size newMediaSize = data.size;
      EdgeDims newMediaPadding = data.padding;
      if (newMediaSize != _mediaSize || newMediaPadding != _mediaPadding) {
        _mediaSize = newMediaSize;
        _mediaPadding = newMediaPadding;
        scheduleMicrotask(_ensureVisibleIfFocused);
      }
    }
Hixie's avatar
Hixie committed
274 275 276 277 278 279 280 281 282
    return new Semantics(
      container: true,
      child: new _FocusScope(
        focusState: this,
        scopeFocused: Focus._atScope(context),
        focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
        focusedWidget: _focusedWidget,
        child: config.child
      )
283 284
    );
  }
Eric Seidel's avatar
Eric Seidel committed
285
}