Commit 4da27671 authored by Eric Seidel's avatar Eric Seidel Committed by Hixie

Focus support.

This patch provides a class to manage focus, Focus, and a class to
manage ownership of the keyboard, KeyboardHandle.

Inherited (in widgets.dart) is adjusted to support subclasses that
need to copy state from instance to instance.

A GlobalKey key type is introduced that is basically the same as
UniqueKey. Component classes that need a globally-unique key can
specify that their 'key' constructor argument is a GlobalKey.

Focus
-----

You can use `Focus.at(this)` to determine if you, a Component, are
currently focused.

You can use `Focus.moveTo(this)` to take the focus or give it to a
particular component.

For this to work, there has to be a Focus class in the widget
hierarchy.

Currently, there can only be one Focus class, because nested scopes
aren't supported. We should add support for that in a future patch.
See issue #229.

KeyboardHandle
--------------

Instead of directly interacting with the keyboard service, you now ask
for a KeyboardHandle using `_keyboard.show(client)`. This returns a
KeyboardHandle class. On that class, you can call `handle.release()`
when you want to hide the keyboard. If `handle.attached` is still
true, and you need to reshow the keyboard after the user hid it, then
you can can `handle.showByRequest()`.

The point of this is that the `keyboard.show()` method maintains the
invariant that only one KeyboardHandle is live at a time.

There are some issues with the keyboard service that I filed as a
result of doing this patch: #226 #227
parent 3331d44f
...@@ -67,6 +67,7 @@ dart_pkg("sky") { ...@@ -67,6 +67,7 @@ dart_pkg("sky") {
"lib/widgets/fixed_height_scrollable.dart", "lib/widgets/fixed_height_scrollable.dart",
"lib/widgets/flat_button.dart", "lib/widgets/flat_button.dart",
"lib/widgets/floating_action_button.dart", "lib/widgets/floating_action_button.dart",
"lib/widgets/focus.dart",
"lib/widgets/icon.dart", "lib/widgets/icon.dart",
"lib/widgets/icon_button.dart", "lib/widgets/icon_button.dart",
"lib/widgets/ink_well.dart", "lib/widgets/ink_well.dart",
......
...@@ -2,35 +2,47 @@ ...@@ -2,35 +2,47 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:sky/editing/input.dart';
import 'package:sky/theme/colors.dart' as colors; import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/theme/typography.dart' as typography; import 'package:sky/theme/typography.dart' as typography;
import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/default_text_style.dart'; import 'package:sky/widgets/default_text_style.dart';
import 'package:sky/widgets/theme.dart';
import 'package:sky/widgets/widget.dart';
import 'package:sky/widgets/task_description.dart';
import 'package:sky/widgets/scaffold.dart';
import 'package:sky/widgets/tool_bar.dart';
import 'package:sky/widgets/icon_button.dart';
import 'package:sky/widgets/floating_action_button.dart'; import 'package:sky/widgets/floating_action_button.dart';
import 'package:sky/widgets/focus.dart';
import 'package:sky/widgets/icon.dart'; import 'package:sky/widgets/icon.dart';
import 'package:sky/widgets/icon_button.dart';
import 'package:sky/widgets/material.dart'; import 'package:sky/widgets/material.dart';
import 'package:sky/editing/input.dart'; import 'package:sky/widgets/scaffold.dart';
import 'package:sky/widgets/scrollable_viewport.dart'; import 'package:sky/widgets/scrollable_viewport.dart';
import 'package:sky/widgets/task_description.dart';
import 'package:sky/widgets/theme.dart';
import 'package:sky/widgets/tool_bar.dart';
import 'package:sky/widgets/widget.dart';
class Field extends Component { class Field extends Component {
Field({this.icon: null, this.placeholder: null}); Field({
Key key,
this.inputKey,
this.icon,
this.placeholder
}): super(key: key);
String icon; final GlobalKey inputKey;
String placeholder; final String icon;
final String placeholder;
Widget build() { Widget build() {
return new Flex([ return new Flex([
new Padding( new Padding(
padding: const EdgeDims.symmetric(horizontal: 16.0), padding: const EdgeDims.symmetric(horizontal: 16.0),
child: new Icon(type:icon, size:24) child: new Icon(type: icon, size: 24)
), ),
new Flexible(child:new Input(placeholder:placeholder, focused:false)) new Flexible(
child: new Input(
key: inputKey,
placeholder: placeholder
)
)
], ],
direction: FlexDirection.horizontal direction: FlexDirection.horizontal
); );
...@@ -53,6 +65,13 @@ class AddressBookApp extends App { ...@@ -53,6 +65,13 @@ class AddressBookApp extends App {
); );
} }
static final GlobalKey nameKey = new GlobalKey();
static final GlobalKey phoneKey = new GlobalKey();
static final GlobalKey emailKey = new GlobalKey();
static final GlobalKey addressKey = new GlobalKey();
static final GlobalKey ringtoneKey = new GlobalKey();
static final GlobalKey noteKey = new GlobalKey();
Widget buildBody() { Widget buildBody() {
return new Material( return new Material(
child: new ScrollableBlock([ child: new ScrollableBlock([
...@@ -62,12 +81,12 @@ class AddressBookApp extends App { ...@@ -62,12 +81,12 @@ class AddressBookApp extends App {
decoration: new BoxDecoration(backgroundColor: colors.Purple[300]) decoration: new BoxDecoration(backgroundColor: colors.Purple[300])
) )
), ),
new Field(icon:"social/person", placeholder:"Name"), new Field(inputKey: nameKey, icon: "social/person", placeholder: "Name"),
new Field(icon: "communication/phone", placeholder:"Phone"), new Field(inputKey: phoneKey, icon: "communication/phone", placeholder: "Phone"),
new Field(icon: "communication/email", placeholder:"Email"), new Field(inputKey: emailKey, icon: "communication/email", placeholder: "Email"),
new Field(icon: "maps/place", placeholder:"Address"), new Field(inputKey: addressKey, icon: "maps/place", placeholder: "Address"),
new Field(icon: "av/volume_up", placeholder:"Ringtone"), new Field(inputKey: ringtoneKey, icon: "av/volume_up", placeholder: "Ringtone"),
new Field(icon: "content/add", placeholder:"Add note"), new Field(inputKey: noteKey, icon: "content/add", placeholder: "Add note"),
]) ])
); );
} }
...@@ -81,24 +100,25 @@ class AddressBookApp extends App { ...@@ -81,24 +100,25 @@ class AddressBookApp extends App {
} }
Widget build() { Widget build() {
ThemeData theme = new ThemeData( ThemeData theme = new ThemeData(
brightness: ThemeBrightness.light, brightness: ThemeBrightness.light,
primarySwatch: colors.Teal, primarySwatch: colors.Teal,
accentColor: colors.PinkAccent[100] accentColor: colors.PinkAccent[100]
); );
return new Theme( return new Theme(
data: theme, data: theme,
child: new DefaultTextStyle( child: new DefaultTextStyle(
style: typography.error, // if you see this, you've forgotten to correctly configure the text style! style: typography.error, // if you see this, you've forgotten to correctly configure the text style!
child: new TaskDescription( child: new TaskDescription(
label: 'Address Book', label: 'Address Book',
child: new Focus(
defaultFocus: nameKey,
child: buildMain() child: buildMain()
) )
) )
); )
} );
}
} }
void main() { void main() {
......
...@@ -10,6 +10,7 @@ import 'package:sky/widgets/drawer_divider.dart'; ...@@ -10,6 +10,7 @@ import 'package:sky/widgets/drawer_divider.dart';
import 'package:sky/widgets/drawer_header.dart'; import 'package:sky/widgets/drawer_header.dart';
import 'package:sky/widgets/drawer_item.dart'; import 'package:sky/widgets/drawer_item.dart';
import 'package:sky/widgets/floating_action_button.dart'; import 'package:sky/widgets/floating_action_button.dart';
import 'package:sky/widgets/focus.dart';
import 'package:sky/widgets/icon.dart'; import 'package:sky/widgets/icon.dart';
import 'package:sky/widgets/icon_button.dart'; import 'package:sky/widgets/icon_button.dart';
import 'package:sky/widgets/modal_overlay.dart'; import 'package:sky/widgets/modal_overlay.dart';
...@@ -244,6 +245,8 @@ class StockHome extends StatefulComponent { ...@@ -244,6 +245,8 @@ class StockHome extends StatefulComponent {
); );
} }
static GlobalKey searchFieldKey = new GlobalKey();
// TODO(abarth): Should we factor this into a SearchBar in the framework? // TODO(abarth): Should we factor this into a SearchBar in the framework?
Widget buildSearchBar() { Widget buildSearchBar() {
return new ToolBar( return new ToolBar(
...@@ -253,7 +256,7 @@ class StockHome extends StatefulComponent { ...@@ -253,7 +256,7 @@ class StockHome extends StatefulComponent {
onPressed: _handleSearchEnd onPressed: _handleSearchEnd
), ),
center: new Input( center: new Input(
focused: true, key: searchFieldKey,
placeholder: 'Search stocks', placeholder: 'Search stocks',
onChanged: _handleSearchQueryChanged onChanged: _handleSearchQueryChanged
), ),
...@@ -318,6 +321,9 @@ class StockHome extends StatefulComponent { ...@@ -318,6 +321,9 @@ class StockHome extends StatefulComponent {
), ),
]; ];
addMenuToOverlays(overlays); addMenuToOverlays(overlays);
return new Stack(overlays); return new Focus(
defaultFocus: searchFieldKey,
child: new Stack(overlays)
);
} }
} }
...@@ -7,6 +7,7 @@ import 'package:sky/editing/editable_text.dart'; ...@@ -7,6 +7,7 @@ import 'package:sky/editing/editable_text.dart';
import 'package:sky/mojo/keyboard.dart'; import 'package:sky/mojo/keyboard.dart';
import 'package:sky/painting/text_style.dart'; import 'package:sky/painting/text_style.dart';
import 'package:sky/widgets/basic.dart'; import 'package:sky/widgets/basic.dart';
import 'package:sky/widgets/focus.dart';
import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/theme.dart';
typedef void ValueChanged(value); typedef void ValueChanged(value);
...@@ -17,23 +18,18 @@ const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0); ...@@ -17,23 +18,18 @@ const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0);
class Input extends StatefulComponent { class Input extends StatefulComponent {
// Current thinking is that Widget will have an optional globalKey Input({
// or heroKey and it will ask Focus.from(this).isFocused which will Key key,
// check using its globalKey. this.placeholder,
// Only one element can use a globalKey at a time and its' up to this.onChanged
// Widget.sync to maintain the mapping. }): super(key: key);
// Never makes sense to have both a localKey and a globalKey.
// Possibly a class HeroKey who functions as a UUID.
Input({Key key,
this.placeholder,
this.onChanged,
this.focused})
: super(key: key);
String placeholder; String placeholder;
ValueChanged onChanged; ValueChanged onChanged;
bool focused = false;
String _value = '';
EditableString _editableValue;
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
void initState() { void initState() {
_editableValue = new EditableString( _editableValue = new EditableString(
...@@ -46,17 +42,13 @@ class Input extends StatefulComponent { ...@@ -46,17 +42,13 @@ class Input extends StatefulComponent {
void syncFields(Input source) { void syncFields(Input source) {
placeholder = source.placeholder; placeholder = source.placeholder;
onChanged = source.onChanged; onChanged = source.onChanged;
focused = source.focused;
} }
String _value = '';
bool _isAttachedToKeyboard = false;
EditableString _editableValue;
void _handleTextUpdated() { void _handleTextUpdated() {
scheduleBuild();
if (_value != _editableValue.text) { if (_value != _editableValue.text) {
_value = _editableValue.text; setState(() {
_value = _editableValue.text;
});
if (onChanged != null) if (onChanged != null)
onChanged(_value); onChanged(_value);
} }
...@@ -64,10 +56,12 @@ class Input extends StatefulComponent { ...@@ -64,10 +56,12 @@ class Input extends StatefulComponent {
Widget build() { Widget build() {
ThemeData themeData = Theme.of(this); ThemeData themeData = Theme.of(this);
bool focused = Focus.at(this);
if (focused && !_isAttachedToKeyboard) { if (focused && !_keyboardHandle.attached) {
keyboard.show(_editableValue.stub); _keyboardHandle = keyboard.show(_editableValue.stub);
_isAttachedToKeyboard = true; } else if (!focused && _keyboardHandle.attached) {
_keyboardHandle.release();
} }
TextStyle textStyle = themeData.text.subhead; TextStyle textStyle = themeData.text.subhead;
...@@ -109,13 +103,23 @@ class Input extends StatefulComponent { ...@@ -109,13 +103,23 @@ class Input extends StatefulComponent {
return new Listener( return new Listener(
child: input, child: input,
onPointerDown: (_) => keyboard.showByRequest() onPointerDown: focus
); );
} }
void focus(_) {
if (Focus.at(this)) {
assert(_keyboardHandle.attached);
_keyboardHandle.showByRequest();
} else {
Focus.moveTo(this);
// we'll get told to rebuild and we'll take care of the keyboard then
}
}
void didUnmount() { void didUnmount() {
if (_isAttachedToKeyboard) if (_keyboardHandle.attached)
keyboard.hide(); _keyboardHandle.release();
super.didUnmount(); super.didUnmount();
} }
} }
...@@ -6,15 +6,74 @@ import 'package:mojom/keyboard/keyboard.mojom.dart'; ...@@ -6,15 +6,74 @@ import 'package:mojom/keyboard/keyboard.mojom.dart';
import 'package:sky/mojo/shell.dart' as shell; import 'package:sky/mojo/shell.dart' as shell;
class _KeyboardConnection { class _KeyboardConnection {
KeyboardServiceProxy proxy;
_KeyboardConnection() { _KeyboardConnection() {
proxy = new KeyboardServiceProxy.unbound(); proxy = new KeyboardServiceProxy.unbound();
shell.requestService("mojo:keyboard", proxy); shell.requestService("mojo:keyboard", proxy);
} }
KeyboardService get keyboard => proxy.ptr; KeyboardServiceProxy proxy;
KeyboardService get keyboardService => proxy.ptr;
static final _KeyboardConnection instance = new _KeyboardConnection();
}
class Keyboard {
Keyboard(this.service);
// The service is exposed in case you need direct access.
// However, as a general rule, you should be able to do
// most of what you need using only this class.
final KeyboardService service;
KeyboardHandle _currentHandle;
KeyboardHandle show(KeyboardClientStub stub) {
assert(stub != null);
if (_currentHandle != null) {
if (_currentHandle.stub == stub)
return _currentHandle;
_currentHandle.release();
}
_currentHandle = new KeyboardHandle._show(this, stub);
return _currentHandle;
}
}
class KeyboardHandle {
KeyboardHandle._show(Keyboard keyboard, this.stub) : _keyboard = keyboard {
_keyboard.service.show(stub);
_attached = true;
}
KeyboardHandle._unattached(Keyboard keyboard) : _keyboard = keyboard, stub = null, _attached = false;
static final unattached = new KeyboardHandle._unattached(keyboard);
final Keyboard _keyboard;
final KeyboardClientStub stub;
bool _attached;
bool get attached => _attached;
void showByRequest() {
assert(_attached);
assert(_keyboard._currentHandle == this);
_keyboard.service.showByRequest();
}
void release() {
if (_attached) {
assert(_keyboard._currentHandle == this);
_keyboard.service.hide();
_attached = false;
_keyboard._currentHandle = null;
}
assert(_keyboard._currentHandle != this);
}
} }
final _KeyboardConnection _connection = new _KeyboardConnection(); final Keyboard keyboard = new Keyboard(_KeyboardConnection.instance.keyboardService);
final KeyboardService keyboard = _connection.keyboard;
// 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/widget.dart';
class Focus extends Inherited {
// TODO(ianh): This doesn't yet support nested scopes. We should not
// be telling our _currentlyFocusedKey that they are focused if we
// ourselves are not focused. Otherwise if you have a dialog with a
// text field over the top of a pane with a text field, they'll
// fight over control of the keyboard.
Focus({
GlobalKey key,
GlobalKey defaultFocus,
Widget child
}) : super(key: key, child: child);
GlobalKey defaultFocus;
GlobalKey _currentlyFocusedKey;
GlobalKey get currentlyFocusedKey {
if (_currentlyFocusedKey != null)
return _currentlyFocusedKey;
return defaultFocus;
}
void set currentlyFocusedKey(GlobalKey value) {
if (value != _currentlyFocusedKey) {
_currentlyFocusedKey = value;
notifyDescendants();
}
}
void syncState(Focus old) {
_currentlyFocusedKey = old._currentlyFocusedKey;
super.syncState(old);
}
static bool at(Component component) {
assert(component != null);
assert(component.key is GlobalKey);
Focus focus = component.inheritedOfType(Focus);
return focus == null || focus.currentlyFocusedKey == component.key;
}
static void moveTo(Component component) {
assert(component != null);
assert(component.key is GlobalKey);
Focus focus = component.inheritedOfType(Focus);
if (focus != null)
focus.currentlyFocusedKey = component.key;
}
}
...@@ -21,10 +21,7 @@ final bool _shouldLogRenderDuration = false; ...@@ -21,10 +21,7 @@ final bool _shouldLogRenderDuration = false;
typedef Widget Builder(); typedef Widget Builder();
typedef void WidgetTreeWalker(Widget); typedef void WidgetTreeWalker(Widget);
abstract class KeyBase { abstract class Key {
}
abstract class Key extends KeyBase {
Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor
factory Key(String value) => new StringKey(value); factory Key(String value) => new StringKey(value);
factory Key.stringify(Object value) => new StringKey(value.toString()); factory Key.stringify(Object value) => new StringKey(value.toString());
...@@ -43,7 +40,7 @@ class StringKey extends Key { ...@@ -43,7 +40,7 @@ class StringKey extends Key {
class ObjectKey extends Key { class ObjectKey extends Key {
ObjectKey(this.value) : super.constructor(); ObjectKey(this.value) : super.constructor();
final Object value; final Object value;
String toString() => '[Instance of ${value.runtimeType}]'; String toString() => '[${value.runtimeType}(${value.hashCode})]';
bool operator==(other) => other is ObjectKey && identical(other.value, value); bool operator==(other) => other is ObjectKey && identical(other.value, value);
int get hashCode => identityHashCode(value); int get hashCode => identityHashCode(value);
} }
...@@ -53,6 +50,11 @@ class UniqueKey extends Key { ...@@ -53,6 +50,11 @@ class UniqueKey extends Key {
String toString() => '[$hashCode]'; String toString() => '[$hashCode]';
} }
class GlobalKey extends Key {
GlobalKey() : super.constructor();
String toString() => '[Global Key $hashCode]';
}
/// A base class for elements of the widget tree /// A base class for elements of the widget tree
abstract class Widget { abstract class Widget {
...@@ -326,22 +328,29 @@ abstract class Inherited extends TagNode { ...@@ -326,22 +328,29 @@ abstract class Inherited extends TagNode {
Inherited({ Key key, Widget child }) : super._withKey(child, key); Inherited({ Key key, Widget child }) : super._withKey(child, key);
void _sync(Widget old, dynamic slot) { void _sync(Widget old, dynamic slot) {
if (old != null && syncShouldNotify(old)) { if (old != null) {
final Type ourRuntimeType = runtimeType; syncState(old);
void notifyChildren(Widget child) { if (syncShouldNotify(old))
if (child is Component && notifyDescendants();
child._dependencies != null &&
child._dependencies.contains(ourRuntimeType))
child._dependenciesChanged();
if (child.runtimeType != ourRuntimeType)
child.walkChildren(notifyChildren);
}
walkChildren(notifyChildren);
} }
super._sync(old, slot); super._sync(old, slot);
} }
bool syncShouldNotify(Inherited old); void notifyDescendants() {
final Type ourRuntimeType = runtimeType;
void notifyChildren(Widget child) {
if (child is Component &&
child._dependencies != null &&
child._dependencies.contains(ourRuntimeType))
child._dependenciesChanged();
if (child.runtimeType != ourRuntimeType)
child.walkChildren(notifyChildren);
}
walkChildren(notifyChildren);
}
void syncState(Inherited old) { }
bool syncShouldNotify(Inherited old) => false;
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment