Unverified Commit 541bff40 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Text Editing Movement Keys via Shortcuts (#75032)

Text editing shortcuts involving the arrow keys are no longer handled by RenderEditable's RawKeyboardListener, they use the new Shortcuts setup.  First PR in a plan to port all text editing keyboard handling to shortcuts.
parent 57dc5f29
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb; import 'package:flutter/foundation.dart' show defaultTargetPlatform;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
...@@ -1202,7 +1202,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1202,7 +1202,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
), ),
); );
final Widget child = Semantics( return Semantics(
enabled: enabled, enabled: enabled,
onTap: !enabled || widget.readOnly ? null : () { onTap: !enabled || widget.readOnly ? null : () {
if (!controller.selection.isValid) { if (!controller.selection.isValid) {
...@@ -1226,13 +1226,5 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1226,13 +1226,5 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
), ),
), ),
); );
if (kIsWeb) {
return Shortcuts(
shortcuts: scrollShortcutOverrides,
child: child,
);
}
return child;
} }
} }
...@@ -884,7 +884,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -884,7 +884,7 @@ class _MaterialAppState extends State<MaterialApp> {
child: HeroControllerScope( child: HeroControllerScope(
controller: _heroController, controller: _heroController,
child: result, child: result,
) ),
); );
} }
} }
...@@ -1290,7 +1290,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1290,7 +1290,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
semanticsMaxValueLength = null; semanticsMaxValueLength = null;
} }
child = MouseRegion( return MouseRegion(
cursor: effectiveMouseCursor, cursor: effectiveMouseCursor,
onEnter: (PointerEnterEvent event) => _handleHover(true), onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false), onExit: (PointerExitEvent event) => _handleHover(false),
...@@ -1317,13 +1317,5 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1317,13 +1317,5 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
), ),
), ),
); );
if (kIsWeb) {
return Shortcuts(
shortcuts: scrollShortcutOverrides,
child: child,
);
}
return child;
} }
} }
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
// 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 'dart:ui' show hashValues, TextAffinity, TextPosition, TextRange; import 'dart:ui' show hashValues, TextAffinity, TextPosition, TextRange;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
......
...@@ -12,6 +12,8 @@ import 'actions.dart'; ...@@ -12,6 +12,8 @@ import 'actions.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'default_text_editing_actions.dart';
import 'default_text_editing_shortcuts.dart';
import 'focus_traversal.dart'; import 'focus_traversal.dart';
import 'framework.dart'; import 'framework.dart';
import 'localizations.dart'; import 'localizations.dart';
...@@ -861,6 +863,9 @@ class WidgetsApp extends StatefulWidget { ...@@ -861,6 +863,9 @@ class WidgetsApp extends StatefulWidget {
/// The default map of keyboard shortcuts to intents for the application. /// The default map of keyboard shortcuts to intents for the application.
/// ///
/// By default, this is set to [WidgetsApp.defaultShortcuts]. /// By default, this is set to [WidgetsApp.defaultShortcuts].
///
/// Passing this will not replace [DefaultTextEditingShortcuts]. These can be
/// overridden by using a [Shortcuts] widget lower in the widget tree.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
...@@ -910,6 +915,9 @@ class WidgetsApp extends StatefulWidget { ...@@ -910,6 +915,9 @@ class WidgetsApp extends StatefulWidget {
/// the [actions] for this app. You may also add to the bindings, or override /// the [actions] for this app. You may also add to the bindings, or override
/// specific bindings for a widget subtree, by adding your own [Actions] /// specific bindings for a widget subtree, by adding your own [Actions]
/// widget. /// widget.
///
/// Passing this will not replace [DefaultTextEditingActions]. These can be
/// overridden by placing an [Actions] widget lower in the widget tree.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
...@@ -1621,20 +1629,27 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1621,20 +1629,27 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
: _locale!; : _locale!;
assert(_debugCheckLocalizations(appLocale)); assert(_debugCheckLocalizations(appLocale));
return RootRestorationScope( return RootRestorationScope(
restorationId: widget.restorationScopeId, restorationId: widget.restorationScopeId,
child: Shortcuts( child: Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
child: Actions( shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
actions: widget.actions ?? WidgetsApp.defaultActions, // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
child: FocusTraversalGroup( // fall through to the defaultShortcuts.
policy: ReadingOrderTraversalPolicy(), child: DefaultTextEditingShortcuts(
child: _MediaQueryFromWindow( child: Actions(
child: Localizations( actions: widget.actions ?? WidgetsApp.defaultActions,
locale: appLocale, child: DefaultTextEditingActions(
delegates: _localizationsDelegates.toList(), child: FocusTraversalGroup(
child: title, policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow(
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
),
), ),
), ),
), ),
......
...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart'; ...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'actions.dart';
import 'autofill.dart'; import 'autofill.dart';
import 'automatic_keep_alive.dart'; import 'automatic_keep_alive.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -27,8 +26,8 @@ import 'media_query.dart'; ...@@ -27,8 +26,8 @@ import 'media_query.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'shortcuts.dart';
import 'text.dart'; import 'text.dart';
import 'text_editing_action.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
...@@ -54,17 +53,6 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150); ...@@ -54,17 +53,6 @@ const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
// is shown in an obscured text field. // is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3; const int _kObscureShowLatestCharCursorTicks = 3;
/// A map used to disable scrolling shortcuts in text fields.
///
/// This is a temporary fix for: https://github.com/flutter/flutter/issues/74191
final Map<LogicalKeySet, Intent> scrollShortcutOverrides = <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.space): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowUp): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): DoNothingAndStopPropagationIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowRight): DoNothingAndStopPropagationIntent(),
};
/// A controller for an editable text field. /// A controller for an editable text field.
/// ///
/// Whenever the user modifies a text field with an associated /// Whenever the user modifies a text field with an associated
...@@ -1491,7 +1479,7 @@ class EditableText extends StatefulWidget { ...@@ -1491,7 +1479,7 @@ class EditableText extends StatefulWidget {
} }
/// State for a [EditableText]. /// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient { class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient, TextEditingActionTarget {
Timer? _cursorTimer; Timer? _cursorTimer;
bool _targetCursorVisibility = false; bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true); final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
...@@ -2472,6 +2460,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2472,6 +2460,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// ///
/// This property is typically used to notify the renderer of input gestures /// This property is typically used to notify the renderer of input gestures
/// when [RenderEditable.ignorePointer] is true. /// when [RenderEditable.ignorePointer] is true.
@override
RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable; RenderEditable get renderEditable => _editableKey.currentContext!.findRenderObject()! as RenderEditable;
@override @override
......
// Copyright 2014 The Flutter 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/rendering.dart' show RenderEditable;
import 'actions.dart';
import 'editable_text.dart';
import 'focus_manager.dart';
import 'framework.dart';
/// The recipient of a [TextEditingAction].
///
/// TextEditingActions will only be enabled when an implementer of this class is
/// focused.
///
/// See also:
///
/// * [EditableTextState], which implements this and is the most typical
/// target of a TextEditingAction.
abstract class TextEditingActionTarget {
/// The renderer that handles [TextEditingAction]s.
///
/// See also:
///
/// * [EditableTextState.renderEditable], which overrides this.
RenderEditable get renderEditable;
}
/// An [Action] related to editing text.
///
/// Enables itself only when a [TextEditingActionTarget], e.g. [EditableText],
/// is currently focused. The result of this is that when a
/// TextEditingActionTarget is not focused, it will fall through to any
/// non-TextEditingAction that handles the same shortcut. For example,
/// overriding the tab key in [Shortcuts] with a TextEditingAction will only
/// invoke your TextEditingAction when a TextEditingActionTarget is focused,
/// otherwise the default tab behavior will apply.
///
/// The currently focused TextEditingActionTarget is available in the [invoke]
/// method via [textEditingActionTarget].
///
/// See also:
///
/// * [CallbackAction], which is a similar Action type but unrelated to text
/// editing.
abstract class TextEditingAction<T extends Intent> extends ContextAction<T> {
/// Returns the currently focused [TextEditingAction], or null if none is
/// focused.
@protected
TextEditingActionTarget? get textEditingActionTarget {
// If a TextEditingActionTarget is not focused, then ignore this action.
if (primaryFocus?.context == null
|| primaryFocus!.context! is! StatefulElement
|| ((primaryFocus!.context! as StatefulElement).state is! TextEditingActionTarget)) {
return null;
}
return (primaryFocus!.context! as StatefulElement).state as TextEditingActionTarget;
}
@override
bool isEnabled(T intent) {
// The Action is disabled if there is no focused TextEditingActionTarget, or
// if the platform is web, because web lets the browser handle text editing.
return !kIsWeb && textEditingActionTarget != null;
}
}
// Copyright 2014 The Flutter 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 'actions.dart';
/// An [Intent] to send the event straight to the engine, but only if a
/// TextEditingTarget is focused.
///
/// {@template flutter.widgets.TextEditingIntents.seeAlso}
/// See also:
///
/// * [DefaultTextEditingActions], which responds to this [Intent].
/// * [DefaultTextEditingShortcuts], which triggers this [Intent].
/// {@endtemplate}
class DoNothingAndStopPropagationTextIntent extends Intent{
/// Creates an instance of DoNothingAndStopPropagationTextIntent.
const DoNothingAndStopPropagationTextIntent();
}
/// An [Intent] to expand the selection left to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionLeftByLineTextIntent extends Intent {
/// Creates an instance of ExpandSelectionLeftByLineTextIntent.
const ExpandSelectionLeftByLineTextIntent();
}
/// An [Intent] to expand the selection right to the start/end of the current
/// field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of ExpandSelectionRightByLineTextIntent.
const ExpandSelectionRightByLineTextIntent();
}
/// An [Intent] to expand the selection to the end of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionToEndTextIntent extends Intent{
/// Creates an instance of ExpandSelectionToEndTextIntent.
const ExpandSelectionToEndTextIntent();
}
/// An [Intent] to expand the selection to the start of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExpandSelectionToStartTextIntent extends Intent{
/// Creates an instance of ExpandSelectionToStartTextIntent.
const ExpandSelectionToStartTextIntent();
}
/// An [Intent] to extend the selection down by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionDownTextIntent extends Intent{
/// Creates an instance of ExtendSelectionDownTextIntent.
const ExtendSelectionDownTextIntent();
}
/// An [Intent] to extend the selection left to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftByLineTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftByLineTextIntent.
const ExtendSelectionLeftByLineTextIntent();
}
/// An [Intent] to extend the selection left past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftByWordTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftByWordTextIntent.
const ExtendSelectionLeftByWordTextIntent();
}
/// An [Intent] to extend the selection left by one character.
/// platform for the shift + arrow-left key event.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftTextIntent.
const ExtendSelectionLeftTextIntent();
}
/// An [Intent] to extend the selection right to the start/end of the current
/// line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightByLineTextIntent.
const ExtendSelectionRightByLineTextIntent();
}
/// An [Intent] to extend the selection right past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightByWordTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightByWordTextIntent.
const ExtendSelectionRightByWordTextIntent();
}
/// An [Intent] to extend the selection right by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightTextIntent.
const ExtendSelectionRightTextIntent();
}
/// An [Intent] to extend the selection up by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionUpTextIntent extends Intent{
/// Creates an instance of ExtendSelectionUpTextIntent.
const ExtendSelectionUpTextIntent();
}
/// An [Intent] to move the selection down by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionDownTextIntent extends Intent{
/// Creates an instance of MoveSelectionDownTextIntent.
const MoveSelectionDownTextIntent();
}
/// An [Intent] to move the selection left by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftByLineTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftByLineTextIntent.
const MoveSelectionLeftByLineTextIntent();
}
/// An [Intent] to move the selection left past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftByWordTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftByWordTextIntent.
const MoveSelectionLeftByWordTextIntent();
}
/// An [Intent] to move the selection left by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionLeftTextIntent extends Intent{
/// Creates an instance of MoveSelectionLeftTextIntent.
const MoveSelectionLeftTextIntent();
}
/// An [Intent] to move the selection to the start of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionToStartTextIntent extends Intent{
/// Creates an instance of MoveSelectionToStartTextIntent.
const MoveSelectionToStartTextIntent();
}
/// An [Intent] to move the selection right by one line.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightByLineTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightByLineTextIntent.
const MoveSelectionRightByLineTextIntent();
}
/// An [Intent] to move the selection right past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightByWordTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightByWordTextIntent.
const MoveSelectionRightByWordTextIntent();
}
/// An [Intent] to move the selection right by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionRightTextIntent extends Intent{
/// Creates an instance of MoveSelectionRightTextIntent.
const MoveSelectionRightTextIntent();
}
/// An [Intent] to move the selection to the end of the field.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionToEndTextIntent extends Intent{
/// Creates an instance of MoveSelectionToEndTextIntent.
const MoveSelectionToEndTextIntent();
}
/// An [Intent] to move the selection up by one character.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class MoveSelectionUpTextIntent extends Intent{
/// Creates an instance of MoveSelectionUpTextIntent.
const MoveSelectionUpTextIntent();
}
...@@ -194,6 +194,9 @@ abstract class TextSelectionControls { ...@@ -194,6 +194,9 @@ abstract class TextSelectionControls {
return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed; return delegate.selectAllEnabled && delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
} }
// TODO(justinmc): This and other methods should be ported to Actions and
// removed, along with their keyboard shortcut equivalents.
// https://github.com/flutter/flutter/issues/75004
/// Copy the current selection of the text field managed by the given /// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, remove the selected text from the /// `delegate` to the [Clipboard]. Then, remove the selected text from the
/// text field and hide the toolbar. /// text field and hide the toolbar.
......
...@@ -33,6 +33,8 @@ export 'src/widgets/bottom_navigation_bar_item.dart'; ...@@ -33,6 +33,8 @@ export 'src/widgets/bottom_navigation_bar_item.dart';
export 'src/widgets/color_filter.dart'; export 'src/widgets/color_filter.dart';
export 'src/widgets/container.dart'; export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
export 'src/widgets/default_text_editing_actions.dart';
export 'src/widgets/default_text_editing_shortcuts.dart';
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart'; export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/dismissible.dart'; export 'src/widgets/dismissible.dart';
export 'src/widgets/disposable_build_context.dart'; export 'src/widgets/disposable_build_context.dart';
...@@ -116,6 +118,8 @@ export 'src/widgets/spacer.dart'; ...@@ -116,6 +118,8 @@ export 'src/widgets/spacer.dart';
export 'src/widgets/status_transitions.dart'; export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart'; export 'src/widgets/table.dart';
export 'src/widgets/text.dart'; export 'src/widgets/text.dart';
export 'src/widgets/text_editing_action.dart';
export 'src/widgets/text_editing_intents.dart';
export 'src/widgets/text_selection.dart'; export 'src/widgets/text_selection.dart';
export 'src/widgets/text_selection_toolbar_layout_delegate.dart'; export 'src/widgets/text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/texture.dart'; export 'src/widgets/texture.dart';
......
...@@ -186,11 +186,18 @@ void main() { ...@@ -186,11 +186,18 @@ void main() {
' _FocusTraversalGroupMarker\n' ' _FocusTraversalGroupMarker\n'
' FocusTraversalGroup\n' ' FocusTraversalGroup\n'
' _ActionsMarker\n' ' _ActionsMarker\n'
' DefaultTextEditingActions\n'
' _ActionsMarker\n'
' Actions\n' ' Actions\n'
' _ShortcutsMarker\n' ' _ShortcutsMarker\n'
' Semantics\n' ' Semantics\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' DefaultTextEditingShortcuts\n'
' _ShortcutsMarker\n'
' Semantics\n'
' _FocusMarker\n'
' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' UnmanagedRestorationScope\n' ' UnmanagedRestorationScope\n'
' RestorationScope\n' ' RestorationScope\n'
......
...@@ -78,14 +78,18 @@ Widget overlayWithEntry(OverlayEntry entry) { ...@@ -78,14 +78,18 @@ Widget overlayWithEntry(OverlayEntry entry) {
WidgetsLocalizationsDelegate(), WidgetsLocalizationsDelegate(),
MaterialLocalizationsDelegate(), MaterialLocalizationsDelegate(),
], ],
child: Directionality( child: DefaultTextEditingShortcuts(
textDirection: TextDirection.ltr, child: DefaultTextEditingActions(
child: MediaQuery( child: Directionality(
data: const MediaQueryData(size: Size(800.0, 600.0)), textDirection: TextDirection.ltr,
child: Overlay( child: MediaQuery(
initialEntries: <OverlayEntry>[ data: const MediaQueryData(size: Size(800.0, 600.0)),
entry, child: Overlay(
], initialEntries: <OverlayEntry>[
entry,
],
),
),
), ),
), ),
), ),
......
...@@ -39,7 +39,7 @@ void main() { ...@@ -39,7 +39,7 @@ void main() {
expect(find.byKey(key), findsOneWidget); expect(find.byKey(key), findsOneWidget);
}); });
testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async { testWidgets('WidgetsApp default key bindings', (WidgetTester tester) async {
bool? checked = false; bool? checked = false;
final GlobalKey key = GlobalKey(); final GlobalKey key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -64,9 +64,12 @@ void main() { ...@@ -64,9 +64,12 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Default key mapping worked. // Default key mapping worked.
expect(checked, isTrue); expect(checked, isTrue);
checked = false; });
testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async {
final TestAction action = TestAction(); final TestAction action = TestAction();
bool? checked = false;
final GlobalKey key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
WidgetsApp( WidgetsApp(
key: key, key: key,
......
...@@ -4181,7 +4181,7 @@ void main() { ...@@ -4181,7 +4181,7 @@ void main() {
reason: 'on $platform', reason: 'on $platform',
); );
// Select to the beginning of the line. // Select to the beginning of the first line.
await sendKeys( await sendKeys(
tester, tester,
<LogicalKeyboardKey>[ <LogicalKeyboardKey>[
...@@ -4196,8 +4196,8 @@ void main() { ...@@ -4196,8 +4196,8 @@ void main() {
selection, selection,
equals( equals(
const TextSelection( const TextSelection(
baseOffset: 20, baseOffset: 0,
extentOffset: 55, extentOffset: 72,
affinity: TextAffinity.downstream, affinity: TextAffinity.downstream,
), ),
), ),
...@@ -4562,25 +4562,12 @@ void main() { ...@@ -4562,25 +4562,12 @@ void main() {
expect(controller.text, isEmpty, reason: 'on $platform'); expect(controller.text, isEmpty, reason: 'on $platform');
} }
testWidgets('keyboard text selection works as expected on linux', (WidgetTester tester) async { testWidgets('keyboard text selection works', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'linux'); final String targetPlatform = defaultTargetPlatform.toString();
final String platform = targetPlatform.substring(targetPlatform.indexOf('.') + 1).toLowerCase();
await testTextEditing(tester, platform: platform);
// On web, using keyboard for selection is handled by the browser. // On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb); }, skip: kIsWeb, variant: TargetPlatformVariant.all());
testWidgets('keyboard text selection works as expected on android', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'android');
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('keyboard text selection works as expected on fuchsia', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'fuchsia');
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('keyboard text selection works as expected on macos', (WidgetTester tester) async {
await testTextEditing(tester, platform: 'macos');
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('keyboard shortcuts respect read-only', (WidgetTester tester) async { testWidgets('keyboard shortcuts respect read-only', (WidgetTester tester) async {
final String platform = describeEnum(defaultTargetPlatform).toLowerCase(); final String platform = describeEnum(defaultTargetPlatform).toLowerCase();
...@@ -7182,6 +7169,172 @@ void main() { ...@@ -7182,6 +7169,172 @@ void main() {
expect(tester.takeException(), null); expect(tester.takeException(), null);
}); });
testWidgets('can change behavior by overriding text editing shortcuts', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionRightTextIntent(),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
// The right arrow key moves to the right as usual.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 1);
// And the left arrow also moves to the right due to the Shortcuts override.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 2);
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('can change behavior by overriding text editing actions', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
late final bool myIntentWasCalled;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: Actions(
actions: <Type, Action<Intent>>{
MoveSelectionLeftTextIntent: _MyMoveSelectionRightTextAction(
onInvoke: () {
myIntentWasCalled = true;
},
),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
// The right arrow key moves to the right as usual.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 1);
// And the left arrow also moves to the right due to the Actions override.
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pump();
expect(controller.selection.isCollapsed, isTrue);
expect(controller.selection.baseOffset, 2);
expect(myIntentWasCalled, isTrue);
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('ignore key event from web platform', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'test\ntest',
);
controller.selection = const TextSelection(
baseOffset: 0,
extentOffset: 0,
affinity: TextAffinity.upstream,
);
bool myIntentWasCalled = false;
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: Actions(
actions: <Type, Action<Intent>>{
MoveSelectionRightTextIntent: _MyMoveSelectionRightTextAction(
onInvoke: () {
myIntentWasCalled = true;
},
),
},
child: EditableText(
maxLines: 10,
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
),
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
if (kIsWeb) {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'web');
await tester.pump();
expect(myIntentWasCalled, isFalse);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
} else {
await simulateKeyDownEvent(LogicalKeyboardKey.arrowRight, platform: 'android');
await tester.pump();
expect(myIntentWasCalled, isTrue);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 1);
}
});
} }
class UnsettableController extends TextEditingController { class UnsettableController extends TextEditingController {
...@@ -7410,3 +7563,17 @@ class _AccentColorTextEditingController extends TextEditingController { ...@@ -7410,3 +7563,17 @@ class _AccentColorTextEditingController extends TextEditingController {
return super.buildTextSpan(context: context, style: TextStyle(color: color), withComposing: withComposing); return super.buildTextSpan(context: context, style: TextStyle(color: color), withComposing: withComposing);
} }
} }
class _MyMoveSelectionRightTextAction extends TextEditingAction<Intent> {
_MyMoveSelectionRightTextAction({
required this.onInvoke,
}) : super();
final VoidCallback onInvoke;
@override
Object? invoke(Intent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.moveSelectionRight(SelectionChangedCause.keyboard);
onInvoke();
}
}
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