Unverified Commit 387e2b06 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add actions and keyboard shortcut map support (#33298)

This implements the keyboard shortcut handling and action invocation in order to provide a place in the infrastructure for keyboard events to trigger actions. This will allow binding of key events to actions like "move the focus to the next widget" and "activate button".
parent a35d6615
This diff is collapsed.
......@@ -138,8 +138,8 @@ class _FocusDemoState extends State<FocusDemo> {
],
),
OutlineButton(onPressed: () => print('pressed'), child: const Text('PRESS ME')),
Padding(
padding: const EdgeInsets.all(8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
......
......@@ -76,8 +76,8 @@ class _HoverDemoState extends State<HoverDemo> {
),
],
),
Padding(
padding: const EdgeInsets.all(8.0),
const Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true),
),
......
......@@ -11,6 +11,19 @@
import 'package:flutter/foundation.dart';
/// A base class for all keyboard key types.
///
/// See also:
///
/// * [PhysicalKeyboardKey], a class with static values that describe the keys
/// that are returned from [RawKeyEvent.physicalKey].
/// * [LogicalKeyboardKey], a class with static values that describe the keys
/// that are returned from [RawKeyEvent.logicalKey].
abstract class KeyboardKey extends Diagnosticable {
/// A const constructor so that subclasses may be const.
const KeyboardKey();
}
/// A class with static values that describe the keys that are returned from
/// [RawKeyEvent.logicalKey].
///
......@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart';
/// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events.
class LogicalKeyboardKey extends Diagnosticable {
class LogicalKeyboardKey extends KeyboardKey {
/// Creates a LogicalKeyboardKey object with an optional key label and debug
/// name.
///
......@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable {
/// for keys which are not recognized.
static const int autogeneratedMask = 0x10000000000;
/// Mask for the synonym pseudo-keys generated for keys which appear in more
/// than one place on the keyboard.
///
/// IDs in this range are used to represent keys which appear in multiple
/// places on the keyboard, such as the SHIFT, ALT, CTRL, and numeric keypad
/// keys. These key codes will never be generated by the key event system, but
/// may be used in key maps to represent the union of all the keys of each
/// type in order to match them.
///
/// To look up the synonyms that are defined, look in the [synonyms] map.
static const int synonymMask = 0x20000000000;
/// The code prefix for keys which have a Unicode representation.
///
/// This is used by platform-specific code to generate Flutter key codes.
......@@ -362,7 +387,7 @@ class LogicalKeyboardKey extends Diagnosticable {
/// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events.
class PhysicalKeyboardKey extends Diagnosticable {
class PhysicalKeyboardKey extends KeyboardKey {
/// Creates a PhysicalKeyboardKey object with an optional debug name.
///
/// The [usbHidUsage] must not be null.
......
......@@ -11,6 +11,19 @@
import 'package:flutter/foundation.dart';
/// A base class for all keyboard key types.
///
/// See also:
///
/// * [PhysicalKeyboardKey], a class with static values that describe the keys
/// that are returned from [RawKeyEvent.physicalKey].
/// * [LogicalKeyboardKey], a class with static values that describe the keys
/// that are returned from [RawKeyEvent.logicalKey].
abstract class KeyboardKey extends Diagnosticable {
/// A const constructor so that subclasses may be const.
const KeyboardKey();
}
/// A class with static values that describe the keys that are returned from
/// [RawKeyEvent.logicalKey].
///
......@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart';
/// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events.
class LogicalKeyboardKey extends Diagnosticable {
class LogicalKeyboardKey extends KeyboardKey {
/// Creates a LogicalKeyboardKey object with an optional key label and debug
/// name.
///
......@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable {
/// for keys which are not recognized.
static const int autogeneratedMask = 0x10000000000;
/// Mask for the synonym pseudo-keys generated for keys which appear in more
/// than one place on the keyboard.
///
/// IDs in this range are used to represent keys which appear in multiple
/// places on the keyboard, such as the SHIFT, ALT, CTRL, and numeric keypad
/// keys. These key codes will never be generated by the key event system, but
/// may be used in key maps to represent the union of all the keys of each
/// type in order to match them.
///
/// To look up the synonyms that are defined, look in the [synonyms] map.
static const int synonymMask = 0x20000000000;
/// The code prefix for keys which have a Unicode representation.
///
/// This is used by platform-specific code to generate Flutter key codes.
......@@ -1805,7 +1830,7 @@ class LogicalKeyboardKey extends Diagnosticable {
/// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events.
class PhysicalKeyboardKey extends Diagnosticable {
class PhysicalKeyboardKey extends KeyboardKey {
/// Creates a PhysicalKeyboardKey object with an optional debug name.
///
/// The [usbHidUsage] must not be null.
......
......@@ -233,7 +233,7 @@ abstract class RawKeyEventData {
/// * [RawKeyboard], which uses this interface to expose key data.
/// * [RawKeyboardListener], a widget that listens for raw key events.
@immutable
abstract class RawKeyEvent {
abstract class RawKeyEvent extends Diagnosticable {
/// Initializes fields for subclasses, and provides a const constructor for
/// const subclasses.
const RawKeyEvent({
......@@ -406,6 +406,13 @@ abstract class RawKeyEvent {
/// Platform-specific information about the key event.
final RawKeyEventData data;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<LogicalKeyboardKey>('logicalKey', logicalKey));
properties.add(DiagnosticsProperty<PhysicalKeyboardKey>('physicalKey', physicalKey));
}
}
/// The user has pressed a key on the keyboard.
......
This diff is collapsed.
......@@ -450,13 +450,13 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// * [Focus.isAt], which is a static method that will return the focus
/// state of the nearest ancestor [Focus] widget's focus node.
bool get hasFocus {
if (_manager?._currentFocus == null) {
if (_manager?.primaryFocus == null) {
return false;
}
if (hasPrimaryFocus) {
return true;
}
return _manager._currentFocus.ancestors.contains(this);
return _manager.primaryFocus.ancestors.contains(this);
}
/// Returns true if this node currently has the application-wide input focus.
......@@ -473,7 +473,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// receive key events through its [onKey] handler.
///
/// This object notifies its listeners whenever this value changes.
bool get hasPrimaryFocus => _manager?._currentFocus == this;
bool get hasPrimaryFocus => _manager?.primaryFocus == this;
/// Returns the nearest enclosing scope node above this node, including
/// this node, if it's a scope.
......@@ -554,7 +554,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
if (hasFocus) {
// If we are in the focus chain, but not the primary focus, then unfocus
// the primary instead.
_manager._currentFocus.unfocus();
_manager.primaryFocus.unfocus();
}
}
......@@ -639,7 +639,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
child._updateManager(_manager);
if (hadFocus) {
// Update the focus chain for the current focus without changing it.
_manager?._currentFocus?._setAsFocusedChild();
_manager?.primaryFocus?._setAsFocusedChild();
}
if (oldScope != null && child.context != null && child.enclosingScope != oldScope) {
DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope);
......@@ -722,12 +722,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
_markAsDirty(newFocus: this);
}
// Sets this node as the focused child for the enclosing scope, and that scope
// as the focused child for the scope above it, etc., until it reaches the
// root node. It doesn't change the primary focus, it just changes what node
// would be focused if the enclosing scope receives focus, and keeps track of
// previously focused children so that if one is removed, the previous focus
// returns.
/// Sets this node as the [FocusScopeNode.focusedChild] of the enclosing
/// scope.
///
/// Sets this node as the focused child for the enclosing scope, and that
/// scope as the focused child for the scope above it, etc., until it reaches
/// the root node. It doesn't change the primary focus, it just changes what
/// node would be focused if the enclosing scope receives focus, and keeps
/// track of previously focused children in that scope, so that if the focused
/// child in that scope is removed, the previous focus returns.
void _setAsFocusedChild() {
FocusNode scopeFocus = this;
for (FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
......@@ -957,7 +960,7 @@ class FocusManager with DiagnosticableTreeMixin {
void _handleRawKeyEvent(RawKeyEvent event) {
// Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it, stop.
if (_currentFocus == null) {
if (_primaryFocus == null) {
return;
}
Iterable<FocusNode> allNodes(FocusNode node) sync* {
......@@ -967,15 +970,17 @@ class FocusManager with DiagnosticableTreeMixin {
}
}
for (FocusNode node in allNodes(_currentFocus)) {
for (FocusNode node in allNodes(_primaryFocus)) {
if (node.onKey != null && node.onKey(node, event)) {
break;
}
}
}
// The node that currently has the primary focus.
FocusNode _currentFocus;
/// The node that currently has the primary focus.
FocusNode get primaryFocus => _primaryFocus;
FocusNode _primaryFocus;
// The node that has requested to have the primary focus, but hasn't been
// given it yet.
FocusNode _nextFocus;
......@@ -994,8 +999,8 @@ class FocusManager with DiagnosticableTreeMixin {
// pending request to be focused should be canceled.
void _willUnfocusNode(FocusNode node) {
assert(node != null);
if (_currentFocus == node) {
_currentFocus = null;
if (_primaryFocus == node) {
_primaryFocus = null;
_dirtyNodes.add(node);
_markNeedsUpdate();
}
......@@ -1024,14 +1029,14 @@ class FocusManager with DiagnosticableTreeMixin {
void _applyFocusChange() {
_haveScheduledUpdate = false;
final FocusNode previousFocus = _currentFocus;
if (_currentFocus == null && _nextFocus == null) {
final FocusNode previousFocus = _primaryFocus;
if (_primaryFocus == null && _nextFocus == null) {
// If we don't have any current focus, and nobody has asked to focus yet,
// then pick a first one using widget order as a default.
_nextFocus = rootScope;
}
if (_nextFocus != null && _nextFocus != _currentFocus) {
_currentFocus = _nextFocus;
if (_nextFocus != null && _nextFocus != _primaryFocus) {
_primaryFocus = _nextFocus;
final Set<FocusNode> previousPath = previousFocus?.ancestors?.toSet() ?? <FocusNode>{};
final Set<FocusNode> nextPath = _nextFocus.ancestors.toSet();
// Notify nodes that are newly focused.
......@@ -1040,12 +1045,12 @@ class FocusManager with DiagnosticableTreeMixin {
_dirtyNodes.addAll(previousPath.difference(nextPath));
_nextFocus = null;
}
if (previousFocus != _currentFocus) {
if (previousFocus != _primaryFocus) {
if (previousFocus != null) {
_dirtyNodes.add(previousFocus);
}
if (_currentFocus != null) {
_dirtyNodes.add(_currentFocus);
if (_primaryFocus != null) {
_dirtyNodes.add(_primaryFocus);
}
}
for (FocusNode node in _dirtyNodes) {
......@@ -1064,7 +1069,7 @@ class FocusManager with DiagnosticableTreeMixin {
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED'));
properties.add(DiagnosticsProperty<FocusNode>('currentFocus', _currentFocus, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('currentFocus', primaryFocus, defaultValue: null));
}
}
......
This diff is collapsed.
......@@ -14,6 +14,7 @@ library widgets;
export 'package:vector_math/vector_math_64.dart' show Matrix4;
export 'src/widgets/actions.dart';
export 'src/widgets/animated_cross_fade.dart';
export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart';
......@@ -87,6 +88,7 @@ export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollbar.dart';
export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/shortcuts.dart';
export 'src/widgets/single_child_scroll_view.dart';
export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/sliver.dart';
......
This diff is collapsed.
// Copyright 2019 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:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group(LogicalKeySet, () {
test('$LogicalKeySet passes parameters correctly.', () {
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
final LogicalKeySet set2 = LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
);
final LogicalKeySet set3 = LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
);
final LogicalKeySet set4 = LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyD,
);
final LogicalKeySet setFromSet = LogicalKeySet.fromSet(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyD,
});
expect(
set1.keys,
equals(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
}));
expect(
set2.keys,
equals(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
}));
expect(
set3.keys,
equals(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
}));
expect(
set4.keys,
equals(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyD,
}));
expect(
setFromSet.keys,
equals(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyD,
}));
});
test('$LogicalKeySet works as a map key.', () {
final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA);
final LogicalKeySet set2 = LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
);
final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'};
expect(map.containsKey(set1), isTrue);
expect(map.containsKey(LogicalKeySet(LogicalKeyboardKey.keyA)), isTrue);
expect(
set2,
equals(LogicalKeySet.fromSet(<LogicalKeyboardKey>{
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
})));
});
test('$KeySet diagnostics work.', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
})
.map((DiagnosticsNode node) => node.toString())
.toList();
expect(description.length, equals(1));
expect(
description[0],
equalsIgnoringHashCodes(
'keys: {LogicalKeyboardKey#00000(keyId: "0x00000061", keyLabel: "a", debugName: "Key A"), LogicalKeyboardKey#00000(keyId: "0x00000062", keyLabel: "b", debugName: "Key B")}'));
});
});
group(ShortcutManager, () {
test('$ShortcutManager .', () {
});
});
group(Shortcuts, () {});
}
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="manual_tests - actions" type="FlutterRunConfigurationType" factoryName="Flutter" singleton="false">
<option name="filePath" value="$PROJECT_DIR$/dev/manual_tests/lib/actions.dart" />
<method v="2" />
</configuration>
</component>
\ No newline at end of file
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