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> { ...@@ -138,8 +138,8 @@ class _FocusDemoState extends State<FocusDemo> {
], ],
), ),
OutlineButton(onPressed: () => print('pressed'), child: const Text('PRESS ME')), OutlineButton(onPressed: () => print('pressed'), child: const Text('PRESS ME')),
Padding( const Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: TextField( child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true), decoration: InputDecoration(labelText: 'Enter Text', filled: true),
), ),
......
...@@ -76,8 +76,8 @@ class _HoverDemoState extends State<HoverDemo> { ...@@ -76,8 +76,8 @@ class _HoverDemoState extends State<HoverDemo> {
), ),
], ],
), ),
Padding( const Padding(
padding: const EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: TextField( child: TextField(
decoration: InputDecoration(labelText: 'Enter Text', filled: true), decoration: InputDecoration(labelText: 'Enter Text', filled: true),
), ),
......
...@@ -11,6 +11,19 @@ ...@@ -11,6 +11,19 @@
import 'package:flutter/foundation.dart'; 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 /// A class with static values that describe the keys that are returned from
/// [RawKeyEvent.logicalKey]. /// [RawKeyEvent.logicalKey].
/// ///
...@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart'; ...@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart';
/// to keyboard events. /// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for /// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events. /// keyboard events.
class LogicalKeyboardKey extends Diagnosticable { class LogicalKeyboardKey extends KeyboardKey {
/// Creates a LogicalKeyboardKey object with an optional key label and debug /// Creates a LogicalKeyboardKey object with an optional key label and debug
/// name. /// name.
/// ///
...@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable { ...@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable {
/// for keys which are not recognized. /// for keys which are not recognized.
static const int autogeneratedMask = 0x10000000000; 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. /// The code prefix for keys which have a Unicode representation.
/// ///
/// This is used by platform-specific code to generate Flutter key codes. /// This is used by platform-specific code to generate Flutter key codes.
...@@ -362,7 +387,7 @@ class LogicalKeyboardKey extends Diagnosticable { ...@@ -362,7 +387,7 @@ class LogicalKeyboardKey extends Diagnosticable {
/// to keyboard events. /// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for /// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events. /// keyboard events.
class PhysicalKeyboardKey extends Diagnosticable { class PhysicalKeyboardKey extends KeyboardKey {
/// Creates a PhysicalKeyboardKey object with an optional debug name. /// Creates a PhysicalKeyboardKey object with an optional debug name.
/// ///
/// The [usbHidUsage] must not be null. /// The [usbHidUsage] must not be null.
......
...@@ -11,6 +11,19 @@ ...@@ -11,6 +11,19 @@
import 'package:flutter/foundation.dart'; 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 /// A class with static values that describe the keys that are returned from
/// [RawKeyEvent.logicalKey]. /// [RawKeyEvent.logicalKey].
/// ///
...@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart'; ...@@ -108,7 +121,7 @@ import 'package:flutter/foundation.dart';
/// to keyboard events. /// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for /// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events. /// keyboard events.
class LogicalKeyboardKey extends Diagnosticable { class LogicalKeyboardKey extends KeyboardKey {
/// Creates a LogicalKeyboardKey object with an optional key label and debug /// Creates a LogicalKeyboardKey object with an optional key label and debug
/// name. /// name.
/// ///
...@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable { ...@@ -249,6 +262,18 @@ class LogicalKeyboardKey extends Diagnosticable {
/// for keys which are not recognized. /// for keys which are not recognized.
static const int autogeneratedMask = 0x10000000000; 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. /// The code prefix for keys which have a Unicode representation.
/// ///
/// This is used by platform-specific code to generate Flutter key codes. /// This is used by platform-specific code to generate Flutter key codes.
...@@ -1805,7 +1830,7 @@ class LogicalKeyboardKey extends Diagnosticable { ...@@ -1805,7 +1830,7 @@ class LogicalKeyboardKey extends Diagnosticable {
/// to keyboard events. /// to keyboard events.
/// * [RawKeyboardListener], a widget used to listen to and supply handlers for /// * [RawKeyboardListener], a widget used to listen to and supply handlers for
/// keyboard events. /// keyboard events.
class PhysicalKeyboardKey extends Diagnosticable { class PhysicalKeyboardKey extends KeyboardKey {
/// Creates a PhysicalKeyboardKey object with an optional debug name. /// Creates a PhysicalKeyboardKey object with an optional debug name.
/// ///
/// The [usbHidUsage] must not be null. /// The [usbHidUsage] must not be null.
......
...@@ -233,7 +233,7 @@ abstract class RawKeyEventData { ...@@ -233,7 +233,7 @@ abstract class RawKeyEventData {
/// * [RawKeyboard], which uses this interface to expose key data. /// * [RawKeyboard], which uses this interface to expose key data.
/// * [RawKeyboardListener], a widget that listens for raw key events. /// * [RawKeyboardListener], a widget that listens for raw key events.
@immutable @immutable
abstract class RawKeyEvent { abstract class RawKeyEvent extends Diagnosticable {
/// Initializes fields for subclasses, and provides a const constructor for /// Initializes fields for subclasses, and provides a const constructor for
/// const subclasses. /// const subclasses.
const RawKeyEvent({ const RawKeyEvent({
...@@ -406,6 +406,13 @@ abstract class RawKeyEvent { ...@@ -406,6 +406,13 @@ abstract class RawKeyEvent {
/// Platform-specific information about the key event. /// Platform-specific information about the key event.
final RawKeyEventData data; 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. /// The user has pressed a key on the keyboard.
......
This diff is collapsed.
...@@ -450,13 +450,13 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -450,13 +450,13 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// * [Focus.isAt], which is a static method that will return the focus /// * [Focus.isAt], which is a static method that will return the focus
/// state of the nearest ancestor [Focus] widget's focus node. /// state of the nearest ancestor [Focus] widget's focus node.
bool get hasFocus { bool get hasFocus {
if (_manager?._currentFocus == null) { if (_manager?.primaryFocus == null) {
return false; return false;
} }
if (hasPrimaryFocus) { if (hasPrimaryFocus) {
return true; 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. /// Returns true if this node currently has the application-wide input focus.
...@@ -473,7 +473,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -473,7 +473,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// receive key events through its [onKey] handler. /// receive key events through its [onKey] handler.
/// ///
/// This object notifies its listeners whenever this value changes. /// 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 /// Returns the nearest enclosing scope node above this node, including
/// this node, if it's a scope. /// this node, if it's a scope.
...@@ -554,7 +554,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -554,7 +554,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
if (hasFocus) { if (hasFocus) {
// If we are in the focus chain, but not the primary focus, then unfocus // If we are in the focus chain, but not the primary focus, then unfocus
// the primary instead. // the primary instead.
_manager._currentFocus.unfocus(); _manager.primaryFocus.unfocus();
} }
} }
...@@ -639,7 +639,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -639,7 +639,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
child._updateManager(_manager); child._updateManager(_manager);
if (hadFocus) { if (hadFocus) {
// Update the focus chain for the current focus without changing it. // 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) { if (oldScope != null && child.context != null && child.enclosingScope != oldScope) {
DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope); DefaultFocusTraversal.of(child.context, nullOk: true)?.changedScope(node: child, oldScope: oldScope);
...@@ -722,12 +722,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -722,12 +722,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
_markAsDirty(newFocus: this); _markAsDirty(newFocus: this);
} }
// Sets this node as the focused child for the enclosing scope, and that scope /// Sets this node as the [FocusScopeNode.focusedChild] of the enclosing
// as the focused child for the scope above it, etc., until it reaches the /// scope.
// 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 /// Sets this node as the focused child for the enclosing scope, and that
// previously focused children so that if one is removed, the previous focus /// scope as the focused child for the scope above it, etc., until it reaches
// returns. /// 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() { void _setAsFocusedChild() {
FocusNode scopeFocus = this; FocusNode scopeFocus = this;
for (FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) { for (FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
...@@ -957,7 +960,7 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -957,7 +960,7 @@ class FocusManager with DiagnosticableTreeMixin {
void _handleRawKeyEvent(RawKeyEvent event) { void _handleRawKeyEvent(RawKeyEvent event) {
// Walk the current focus from the leaf to the root, calling each one's // 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. // onKey on the way up, and if one responds that they handled it, stop.
if (_currentFocus == null) { if (_primaryFocus == null) {
return; return;
} }
Iterable<FocusNode> allNodes(FocusNode node) sync* { Iterable<FocusNode> allNodes(FocusNode node) sync* {
...@@ -967,15 +970,17 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -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)) { if (node.onKey != null && node.onKey(node, event)) {
break; break;
} }
} }
} }
// The node that currently has the primary focus. /// The node that currently has the primary focus.
FocusNode _currentFocus; FocusNode get primaryFocus => _primaryFocus;
FocusNode _primaryFocus;
// The node that has requested to have the primary focus, but hasn't been // The node that has requested to have the primary focus, but hasn't been
// given it yet. // given it yet.
FocusNode _nextFocus; FocusNode _nextFocus;
...@@ -994,8 +999,8 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -994,8 +999,8 @@ class FocusManager with DiagnosticableTreeMixin {
// pending request to be focused should be canceled. // pending request to be focused should be canceled.
void _willUnfocusNode(FocusNode node) { void _willUnfocusNode(FocusNode node) {
assert(node != null); assert(node != null);
if (_currentFocus == node) { if (_primaryFocus == node) {
_currentFocus = null; _primaryFocus = null;
_dirtyNodes.add(node); _dirtyNodes.add(node);
_markNeedsUpdate(); _markNeedsUpdate();
} }
...@@ -1024,14 +1029,14 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1024,14 +1029,14 @@ class FocusManager with DiagnosticableTreeMixin {
void _applyFocusChange() { void _applyFocusChange() {
_haveScheduledUpdate = false; _haveScheduledUpdate = false;
final FocusNode previousFocus = _currentFocus; final FocusNode previousFocus = _primaryFocus;
if (_currentFocus == null && _nextFocus == null) { if (_primaryFocus == null && _nextFocus == null) {
// If we don't have any current focus, and nobody has asked to focus yet, // 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. // then pick a first one using widget order as a default.
_nextFocus = rootScope; _nextFocus = rootScope;
} }
if (_nextFocus != null && _nextFocus != _currentFocus) { if (_nextFocus != null && _nextFocus != _primaryFocus) {
_currentFocus = _nextFocus; _primaryFocus = _nextFocus;
final Set<FocusNode> previousPath = previousFocus?.ancestors?.toSet() ?? <FocusNode>{}; final Set<FocusNode> previousPath = previousFocus?.ancestors?.toSet() ?? <FocusNode>{};
final Set<FocusNode> nextPath = _nextFocus.ancestors.toSet(); final Set<FocusNode> nextPath = _nextFocus.ancestors.toSet();
// Notify nodes that are newly focused. // Notify nodes that are newly focused.
...@@ -1040,12 +1045,12 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1040,12 +1045,12 @@ class FocusManager with DiagnosticableTreeMixin {
_dirtyNodes.addAll(previousPath.difference(nextPath)); _dirtyNodes.addAll(previousPath.difference(nextPath));
_nextFocus = null; _nextFocus = null;
} }
if (previousFocus != _currentFocus) { if (previousFocus != _primaryFocus) {
if (previousFocus != null) { if (previousFocus != null) {
_dirtyNodes.add(previousFocus); _dirtyNodes.add(previousFocus);
} }
if (_currentFocus != null) { if (_primaryFocus != null) {
_dirtyNodes.add(_currentFocus); _dirtyNodes.add(_primaryFocus);
} }
} }
for (FocusNode node in _dirtyNodes) { for (FocusNode node in _dirtyNodes) {
...@@ -1064,7 +1069,7 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1064,7 +1069,7 @@ class FocusManager with DiagnosticableTreeMixin {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED')); 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; ...@@ -14,6 +14,7 @@ library widgets;
export 'package:vector_math/vector_math_64.dart' show Matrix4; 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_cross_fade.dart';
export 'src/widgets/animated_list.dart'; export 'src/widgets/animated_list.dart';
export 'src/widgets/animated_size.dart'; export 'src/widgets/animated_size.dart';
...@@ -87,6 +88,7 @@ export 'src/widgets/scroll_view.dart'; ...@@ -87,6 +88,7 @@ export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollbar.dart'; export 'src/widgets/scrollbar.dart';
export 'src/widgets/semantics_debugger.dart'; export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/shortcuts.dart';
export 'src/widgets/single_child_scroll_view.dart'; export 'src/widgets/single_child_scroll_view.dart';
export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/sliver.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