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

7
import 'package:flutter/foundation.dart';
8

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

14
// _noFocusedScope is used by Focus to track the case where none of the Focus
15
// widget's subscopes (e.g. dialogs) are focused. This is distinct from the
16 17 18
// 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
19

20
class _FocusScope extends InheritedWidget {
21 22
  _FocusScope({
    Key key,
23
    this.focusState,
24 25
    this.scopeFocused,
    this.focusedScope,
26
    this.focusedWidget,
27
    @required Widget child,
28 29 30
  }) : super(key: key, child: child) {
    assert(scopeFocused != null);
  }
Eric Seidel's avatar
Eric Seidel committed
31

32 33 34
  /// The state for this focus scope.
  ///
  /// This widget is always our direct parent widget.
35
  final _FocusState focusState;
36 37

  /// Whether this scope is focused in our ancestor focus scope.
Hixie's avatar
Hixie committed
38
  final bool scopeFocused;
39

40
  // These are mutable because we implicitly change them when they're null in
41 42
  // certain cases, basically pretending retroactively that we were constructed
  // with the right keys.
43 44

  /// Which of our descendant scopes is focused, if any.
45
  GlobalKey focusedScope;
46 47

  /// Which of our descendant widgets is focused, if any.
48 49
  GlobalKey focusedWidget;

50 51 52
  // 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.
53 54

  void _setFocusedWidgetIfUnset(GlobalKey key) {
55 56 57
    focusState._setFocusedWidgetIfUnset(key);
    focusedWidget = focusState._focusedWidget;
    focusedScope = focusState._focusedScope == _noFocusedScope ? null : focusState._focusedScope;
58 59
  }

60
  @override
61 62
  bool updateShouldNotify(_FocusScope oldWidget) {
    if (scopeFocused != oldWidget.scopeFocused)
63 64 65
      return true;
    if (!scopeFocused)
      return false;
66
    if (focusedScope != oldWidget.focusedScope)
67 68 69
      return true;
    if (focusedScope != null)
      return false;
70
    if (focusedWidget != oldWidget.focusedWidget)
71 72 73 74
      return true;
    return false;
  }

75
  @override
Hixie's avatar
Hixie committed
76 77 78 79 80 81 82 83 84
  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');
  }
85 86
}

87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
/// A scope for managing the focus state of descendant widgets.
///
/// The focus represents where the user's attention is directed. If the use
/// interacts with the system in a way that isn't visually directed at a
/// particular widget (e.g., by typing on a keyboard), the interaction is
/// directed to the currently focused widget.
///
/// The focus system consists of a tree of Focus widgets, which is embedded in
/// the widget tree. Focus widgets themselves can be focused in their enclosing
/// Focus widget, which means that their subtree is the one that has the current
/// focus. For example, a dialog creates a Focus widget to maintain focus
/// within the dialog.  When the dialog closes, its Focus widget is removed from
/// the tree and focus is restored to whichever other part of the Focus tree
/// previously had focus.
///
/// In addition to tracking which enclosed Focus widget has focus, each Focus
/// widget also tracks a GlobalKey, which represents the currently focused
/// widget in this part of the focus tree. If this Focus widget is the currently
/// focused subtree of the focus system (i.e., the path from it to the root is
/// focused at each level and it hasn't focused any of its enclosed Focus
107
/// widgets), then the widget with this global key actually has the focus in the
108
/// entire system.
109
class Focus extends StatefulWidget {
110 111 112
  /// Creates a scope for managing focus.
  ///
  /// The [key] argument must not be null.
113
  Focus({
114
    @required GlobalKey key,
115
    this.initiallyFocusedScope,
116
    @required this.child,
117
  }) : super(key: key) {
118
    assert(key != null);
119 120
  }

121 122 123 124 125 126 127 128
  /// The global key of the [Focus] widget below this widget in the tree that
  /// will be focused initially.
  ///
  /// If non-null, a [Focus] widget with this key must be added to the tree
  /// before the end of the current microtask in which the [Focus] widget was
  /// initially constructed.
  final GlobalKey initiallyFocusedScope;

129
  /// The widget below this widget in the tree.
130
  final Widget child;
131

132 133 134
  /// The key that currently has focus globally in the entire focus tree.
  ///
  /// This field is always null except in checked mode.
135 136
  static GlobalKey debugOnlyFocusedKey;

137 138 139 140
  /// Whether the focus is current at the given context.
  ///
  /// If autofocus is true, the given context will become focused if no other
  /// widget is already focused.
141
  static bool at(BuildContext context, { bool autofocus: false }) {
142 143 144 145
    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
146
    _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
Hixie's avatar
Hixie committed
147 148
    if (focusScope != null) {
      if (autofocus)
149
        focusScope._setFocusedWidgetIfUnset(context.widget.key);
Hixie's avatar
Hixie committed
150 151
      return focusScope.scopeFocused &&
             focusScope.focusedScope == null &&
152
             focusScope.focusedWidget == context.widget.key;
Hixie's avatar
Hixie committed
153
    }
154 155 156 157
    assert(() {
      if (debugOnlyFocusedKey?.currentContext == null)
        debugOnlyFocusedKey = context.widget.key;
      if (debugOnlyFocusedKey != context.widget.key) {
158 159 160 161 162 163 164
        throw new FlutterError(
          'Missing Focus scope.\n'
          'Two focusable widgets with different keys, $debugOnlyFocusedKey and ${context.widget.key}, '
          'exist in the widget tree simultaneously, but they have no Focus widget ancestor.\n'
          'If you have more than one focusable widget, then you should put them inside a Focus. '
          'Normally, this is done for you using a Route, via Navigator, WidgetsApp, or MaterialApp.'
        );
165 166 167
      }
      return true;
    });
Hixie's avatar
Hixie committed
168 169 170
    return true;
  }

171
  static bool _atScope(BuildContext context) {
172 173 174
    assert(context != null);
    assert(context.widget != null);
    assert(context.widget is Focus);
175
    assert(context.widget.key != null);
Ian Hickson's avatar
Ian Hickson committed
176
    _FocusScope focusScope = context.inheritFromWidgetOfExactType(_FocusScope);
Hixie's avatar
Hixie committed
177 178
    if (focusScope != null) {
      return focusScope.scopeFocused &&
179
             focusScope.focusedScope == context.widget.key;
Hixie's avatar
Hixie committed
180 181 182 183
    }
    return true;
  }

184 185
  /// Focuses a particular widget, identified by its GlobalKey.
  /// The widget must be in the widget tree.
186
  ///
187 188 189
  /// 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) {
190 191
    BuildContext focusedContext = key.currentContext;
    assert(focusedContext != null);
Ian Hickson's avatar
Ian Hickson committed
192
    _FocusScope focusScope = key.currentContext.ancestorWidgetOfExactType(_FocusScope);
193
    if (focusScope != null) {
194
      focusScope.focusState._setFocusedWidget(key);
195
      Scrollable.ensureVisible(focusedContext);
196
      Scrollable2.ensureVisible(focusedContext);
197
    }
198 199
  }

200 201
  /// Unfocuses the currently focused widget (if any) in the Focus that most
  /// tightly encloses the given context.
202 203 204 205 206 207
  static void clear(BuildContext context) {
    _FocusScope focusScope = context.ancestorWidgetOfExactType(_FocusScope);
    if (focusScope != null)
      focusScope.focusState._clearFocusedWidget();
  }

208
  /// Focuses a particular focus scope, identified by its GlobalKey.
209
  ///
210 211
  /// 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.
212 213 214 215 216 217 218 219 220 221
  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
222
    if (focusScope != null)
223
      focusScope.focusState._setFocusedScope(key);
Hixie's avatar
Hixie committed
224 225
  }

226
  @override
227
  _FocusState createState() => new _FocusState();
228
}
229

230
class _FocusState extends State<Focus> {
231
  @override
232 233
  void initState() {
    super.initState();
234
    _focusedScope = config.initiallyFocusedScope;
235 236
    _updateWidgetRemovalListener(_focusedWidget);
    _updateScopeRemovalListener(_focusedScope);
237 238 239 240 241 242

    assert(() {
      if (_focusedScope != null)
        scheduleMicrotask(_debugCheckInitiallyFocusedScope);
      return true;
    });
243 244
  }

245
  @override
246 247 248 249 250 251
  void dispose() {
    _updateWidgetRemovalListener(null);
    _updateScopeRemovalListener(null);
    super.dispose();
  }

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  void _debugCheckInitiallyFocusedScope() {
    assert(config.initiallyFocusedScope != null);
    assert(() {
      if (!mounted)
        return true;
      Widget widget = config.initiallyFocusedScope.currentWidget;
      if (widget == null) {
        throw new FlutterError(
          'The initially focused scope is not in the tree.\n'
          'When a Focus widget is given an initially focused scope, that focus '
          'scope must be added to the tree before the end of the microtask in '
          'which the Focus widget was first built. However, it is the end of '
          'the microtask and ${config.initiallyFocusedScope} is not in the '
          'tree.'
        );
      }
      if (widget is! Focus) {
        throw new FlutterError(
          'The initially focused scope was not a Focus widget.\n'
          'The initially focused scope for a Focus widget must be another '
          'Focus widget. Instead, the initially focused scope was a '
          '${widget.runtimeType} widget.'
        );
      }
      return true;
    });
  }

280
  GlobalKey _focusedWidget; // when null, the first widget to ask if it's focused will get the focus
281
  GlobalKey _currentlyRegisteredWidgetRemovalListenerKey;
Eric Seidel's avatar
Eric Seidel committed
282

283 284 285 286 287 288 289
  void _setFocusedWidget(GlobalKey key) {
    setState(() {
      _focusedWidget = key;
      if (_focusedScope == null)
        _focusedScope = _noFocusedScope;
    });
    _updateWidgetRemovalListener(key);
Eric Seidel's avatar
Eric Seidel committed
290
  }
291 292 293 294 295 296

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

300 301 302 303 304 305 306 307 308
  void _clearFocusedWidget() {
    if (_focusedWidget != null) {
      _updateWidgetRemovalListener(null);
      setState(() {
        _focusedWidget = null;
      });
    }
  }

309
  void _handleWidgetRemoved(GlobalKey key) {
310
    assert(key != null);
311
    assert(_focusedWidget == key);
312
    _clearFocusedWidget();
313 314 315 316 317
  }

  void _updateWidgetRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredWidgetRemovalListenerKey != key) {
      if (_currentlyRegisteredWidgetRemovalListenerKey != null)
318
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredWidgetRemovalListenerKey, _handleWidgetRemoved);
319
      if (key != null)
320
        GlobalKey.registerRemoveListener(key, _handleWidgetRemoved);
321 322 323 324 325 326 327 328 329 330 331 332 333
      _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
334

335 336
  void _scopeRemoved(GlobalKey key) {
    assert(_focusedScope == key);
337
    GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
338 339 340 341 342 343 344 345 346
    _currentlyRegisteredScopeRemovalListenerKey = null;
    setState(() {
      _focusedScope = null;
    });
  }

  void _updateScopeRemovalListener(GlobalKey key) {
    if (_currentlyRegisteredScopeRemovalListenerKey != key) {
      if (_currentlyRegisteredScopeRemovalListenerKey != null)
347
        GlobalKey.unregisterRemoveListener(_currentlyRegisteredScopeRemovalListenerKey, _scopeRemoved);
348
      if (key != null)
349
        GlobalKey.registerRemoveListener(key, _scopeRemoved);
350 351 352 353
      _currentlyRegisteredScopeRemovalListenerKey = key;
    }
  }

354
  Size _mediaSize;
355
  EdgeInsets _mediaPadding;
356 357 358 359 360 361 362 363

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

367
  @override
368
  Widget build(BuildContext context) {
369
    MediaQueryData data = MediaQuery.of(context);
370 371 372 373 374 375
    Size newMediaSize = data.size;
    EdgeInsets newMediaPadding = data.padding;
    if (newMediaSize != _mediaSize || newMediaPadding != _mediaPadding) {
      _mediaSize = newMediaSize;
      _mediaPadding = newMediaPadding;
      scheduleMicrotask(_ensureVisibleIfFocused);
376
    }
Hixie's avatar
Hixie committed
377 378 379 380 381 382 383 384 385
    return new Semantics(
      container: true,
      child: new _FocusScope(
        focusState: this,
        scopeFocused: Focus._atScope(context),
        focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
        focusedWidget: _focusedWidget,
        child: config.child
      )
386 387
    );
  }
Eric Seidel's avatar
Eric Seidel committed
388
}