1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'app.dart';
6library;
7
8import 'package:flutter/foundation.dart';
9import 'package:flutter/painting.dart';
10import 'package:flutter/services.dart';
11
12import 'actions.dart';
13import 'focus_traversal.dart';
14import 'framework.dart';
15import 'scrollable_helpers.dart';
16import 'shortcuts.dart';
17import 'text_editing_intents.dart';
18
19/// A widget with the shortcuts used for the default text editing behavior.
20///
21/// This default behavior can be overridden by placing a [Shortcuts] widget
22/// lower in the widget tree than this. See the [Action] class for an example
23/// of remapping an [Intent] to a custom [Action].
24///
25/// The [Shortcuts] widget usually takes precedence over system keybindings.
26/// Proceed with caution if the shortcut you wish to override is also used by
27/// the system. For example, overriding [LogicalKeyboardKey.backspace] could
28/// cause CJK input methods to discard more text than they should when the
29/// backspace key is pressed during text composition on iOS.
30///
31/// {@macro flutter.widgets.editableText.shortcutsAndTextInput}
32///
33/// {@tool snippet}
34///
35/// This example shows how to use an additional [Shortcuts] widget to override
36/// some default text editing keyboard shortcuts to have new behavior. Instead
37/// of moving the cursor, alt + up/down will change the focused widget.
38///
39/// ```dart
40/// @override
41/// Widget build(BuildContext context) {
42/// // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
43/// // then DefaultTextEditingShortcuts is already being inserted into the
44/// // widget tree.
45/// return const DefaultTextEditingShortcuts(
46/// child: Center(
47/// child: Shortcuts(
48/// shortcuts: <ShortcutActivator, Intent>{
49/// SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): NextFocusIntent(),
50/// SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): PreviousFocusIntent(),
51/// },
52/// child: Column(
53/// children: <Widget>[
54/// TextField(
55/// decoration: InputDecoration(
56/// hintText: 'alt + down moves to the next field.',
57/// ),
58/// ),
59/// TextField(
60/// decoration: InputDecoration(
61/// hintText: 'And alt + up moves to the previous.',
62/// ),
63/// ),
64/// ],
65/// ),
66/// ),
67/// ),
68/// );
69/// }
70/// ```
71/// {@end-tool}
72///
73/// {@tool snippet}
74///
75/// This example shows how to use an additional [Shortcuts] widget to override
76/// default text editing shortcuts to have completely custom behavior defined by
77/// a custom Intent and Action. Here, the up/down arrow keys increment/decrement
78/// a counter instead of moving the cursor.
79///
80/// ```dart
81/// class IncrementCounterIntent extends Intent {}
82/// class DecrementCounterIntent extends Intent {}
83///
84/// class MyWidget extends StatefulWidget {
85/// const MyWidget({ super.key });
86///
87/// @override
88/// MyWidgetState createState() => MyWidgetState();
89/// }
90///
91/// class MyWidgetState extends State<MyWidget> {
92///
93/// int _counter = 0;
94///
95/// @override
96/// Widget build(BuildContext context) {
97/// // If using WidgetsApp or its descendants MaterialApp or CupertinoApp,
98/// // then DefaultTextEditingShortcuts is already being inserted into the
99/// // widget tree.
100/// return DefaultTextEditingShortcuts(
101/// child: Center(
102/// child: Column(
103/// mainAxisAlignment: MainAxisAlignment.center,
104/// children: <Widget>[
105/// const Text(
106/// 'You have pushed the button this many times:',
107/// ),
108/// Text(
109/// '$_counter',
110/// style: Theme.of(context).textTheme.headlineMedium,
111/// ),
112/// Shortcuts(
113/// shortcuts: <ShortcutActivator, Intent>{
114/// const SingleActivator(LogicalKeyboardKey.arrowUp): IncrementCounterIntent(),
115/// const SingleActivator(LogicalKeyboardKey.arrowDown): DecrementCounterIntent(),
116/// },
117/// child: Actions(
118/// actions: <Type, Action<Intent>>{
119/// IncrementCounterIntent: CallbackAction<IncrementCounterIntent>(
120/// onInvoke: (IncrementCounterIntent intent) {
121/// setState(() {
122/// _counter++;
123/// });
124/// return null;
125/// },
126/// ),
127/// DecrementCounterIntent: CallbackAction<DecrementCounterIntent>(
128/// onInvoke: (DecrementCounterIntent intent) {
129/// setState(() {
130/// _counter--;
131/// });
132/// return null;
133/// },
134/// ),
135/// },
136/// child: const TextField(
137/// maxLines: 2,
138/// decoration: InputDecoration(
139/// hintText: 'Up/down increment/decrement here.',
140/// ),
141/// ),
142/// ),
143/// ),
144/// const TextField(
145/// maxLines: 2,
146/// decoration: InputDecoration(
147/// hintText: 'Up/down behave normally here.',
148/// ),
149/// ),
150/// ],
151/// ),
152/// ),
153/// );
154/// }
155/// }
156/// ```
157/// {@end-tool}
158///
159/// See also:
160///
161/// * [WidgetsApp], which creates a DefaultTextEditingShortcuts.
162class DefaultTextEditingShortcuts extends StatelessWidget {
163 /// Creates a [DefaultTextEditingShortcuts] widget that provides the default text editing
164 /// shortcuts on the current platform.
165 const DefaultTextEditingShortcuts({
166 super.key,
167 required this.child,
168 });
169
170 /// {@macro flutter.widgets.ProxyWidget.child}
171 final Widget child;
172
173 // These shortcuts are shared between all platforms except Apple platforms,
174 // because they use different modifier keys as the line/word modifier.
175 static final Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
176 // Delete Shortcuts.
177 for (final bool pressShift in const <bool>[true, false])
178 ...<SingleActivator, Intent>{
179 SingleActivator(LogicalKeyboardKey.backspace, shift: pressShift): const DeleteCharacterIntent(forward: false),
180 SingleActivator(LogicalKeyboardKey.backspace, control: true, shift: pressShift): const DeleteToNextWordBoundaryIntent(forward: false),
181 SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: pressShift): const DeleteToLineBreakIntent(forward: false),
182 SingleActivator(LogicalKeyboardKey.delete, shift: pressShift): const DeleteCharacterIntent(forward: true),
183 SingleActivator(LogicalKeyboardKey.delete, control: true, shift: pressShift): const DeleteToNextWordBoundaryIntent(forward: true),
184 SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: pressShift): const DeleteToLineBreakIntent(forward: true),
185 },
186
187 // Arrow: Move selection.
188 const SingleActivator(LogicalKeyboardKey.arrowLeft): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
189 const SingleActivator(LogicalKeyboardKey.arrowRight): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
190 const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
191 const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
192
193 // Shift + Arrow: Extend selection.
194 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
195 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
196 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
197 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
198
199 const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
200 const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
201 const SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
202 const SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
203
204 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
205 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
206 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
207 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
208
209 const SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
210 const SingleActivator(LogicalKeyboardKey.arrowRight, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
211
212 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: false),
213 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: false),
214
215 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, control: true): const ExtendSelectionToNextParagraphBoundaryIntent(forward: false, collapseSelection: false),
216 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, control: true): const ExtendSelectionToNextParagraphBoundaryIntent(forward: true, collapseSelection: false),
217
218 // Page Up / Down: Move selection by page.
219 const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
220 const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
221
222 // Shift + Page Up / Down: Extend selection by page.
223 const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
224 const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
225
226 const SingleActivator(LogicalKeyboardKey.keyX, control: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
227 const SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent.copy,
228 const SingleActivator(LogicalKeyboardKey.keyV, control: true): const PasteTextIntent(SelectionChangedCause.keyboard),
229 const SingleActivator(LogicalKeyboardKey.keyA, control: true): const SelectAllTextIntent(SelectionChangedCause.keyboard),
230 const SingleActivator(LogicalKeyboardKey.keyZ, control: true): const UndoTextIntent(SelectionChangedCause.keyboard),
231 const SingleActivator(LogicalKeyboardKey.keyZ, shift: true, control: true): const RedoTextIntent(SelectionChangedCause.keyboard),
232 // These keys should go to the IME when a field is focused, not to other
233 // Shortcuts.
234 const SingleActivator(LogicalKeyboardKey.space): const DoNothingAndStopPropagationTextIntent(),
235 const SingleActivator(LogicalKeyboardKey.enter): const DoNothingAndStopPropagationTextIntent(),
236 };
237
238 // The following key combinations have no effect on text editing on this
239 // platform:
240 // * End
241 // * Home
242 // * Meta + X
243 // * Meta + C
244 // * Meta + V
245 // * Meta + A
246 // * Meta + shift? + Z
247 // * Meta + shift? + arrow down
248 // * Meta + shift? + arrow left
249 // * Meta + shift? + arrow right
250 // * Meta + shift? + arrow up
251 // * Shift + end
252 // * Shift + home
253 // * Meta + shift? + delete
254 // * Meta + shift? + backspace
255 static final Map<ShortcutActivator, Intent> _androidShortcuts = _commonShortcuts;
256
257 static final Map<ShortcutActivator, Intent> _fuchsiaShortcuts = _androidShortcuts;
258
259 static final Map<ShortcutActivator, Intent> _linuxNumpadShortcuts = <ShortcutActivator, Intent>{
260 // When numLock is on, numpad keys shortcuts require shift to be pressed too.
261 const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, numLock: LockState.locked): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
262 const SingleActivator(LogicalKeyboardKey.numpad4, shift: true, numLock: LockState.locked): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
263 const SingleActivator(LogicalKeyboardKey.numpad8, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
264 const SingleActivator(LogicalKeyboardKey.numpad2, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
265
266 const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: false),
267 const SingleActivator(LogicalKeyboardKey.numpad4, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: false),
268 const SingleActivator(LogicalKeyboardKey.numpad8, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: false, collapseSelection: false),
269 const SingleActivator(LogicalKeyboardKey.numpad2, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: true, collapseSelection: false),
270
271 const SingleActivator(LogicalKeyboardKey.numpad9, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
272 const SingleActivator(LogicalKeyboardKey.numpad3, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
273
274 const SingleActivator(LogicalKeyboardKey.numpad7, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
275 const SingleActivator(LogicalKeyboardKey.numpad1, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
276
277 const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true, numLock: LockState.locked): const DeleteCharacterIntent(forward: true),
278 const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true, control: true, numLock: LockState.locked): const DeleteToNextWordBoundaryIntent(forward: true),
279
280 // When numLock is off, numpad keys shortcuts require shift not to be pressed.
281 const SingleActivator(LogicalKeyboardKey.numpad6, numLock: LockState.unlocked): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
282 const SingleActivator(LogicalKeyboardKey.numpad4, numLock: LockState.unlocked): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
283 const SingleActivator(LogicalKeyboardKey.numpad8, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
284 const SingleActivator(LogicalKeyboardKey.numpad2, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
285
286 const SingleActivator(LogicalKeyboardKey.numpad6, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
287 const SingleActivator(LogicalKeyboardKey.numpad4, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
288 const SingleActivator(LogicalKeyboardKey.numpad8, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: false, collapseSelection: true),
289 const SingleActivator(LogicalKeyboardKey.numpad2, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: true, collapseSelection: true),
290
291 const SingleActivator(LogicalKeyboardKey.numpad9, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
292 const SingleActivator(LogicalKeyboardKey.numpad3, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
293
294 const SingleActivator(LogicalKeyboardKey.numpad7, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
295 const SingleActivator(LogicalKeyboardKey.numpad1, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
296
297 const SingleActivator(LogicalKeyboardKey.numpadDecimal, numLock: LockState.unlocked): const DeleteCharacterIntent(forward: true),
298 const SingleActivator(LogicalKeyboardKey.numpadDecimal, control: true, numLock: LockState.unlocked): const DeleteToNextWordBoundaryIntent(forward: true),
299 };
300
301 static final Map<ShortcutActivator, Intent> _linuxShortcuts = <ShortcutActivator, Intent>{
302 ..._commonShortcuts,
303 ..._linuxNumpadShortcuts,
304 const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
305 const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
306 const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
307 const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
308 // The following key combinations have no effect on text editing on this
309 // platform:
310 // * Control + shift? + end
311 // * Control + shift? + home
312 // * Meta + X
313 // * Meta + C
314 // * Meta + V
315 // * Meta + A
316 // * Meta + shift? + Z
317 // * Meta + shift? + arrow down
318 // * Meta + shift? + arrow left
319 // * Meta + shift? + arrow right
320 // * Meta + shift? + arrow up
321 // * Meta + shift? + delete
322 // * Meta + shift? + backspace
323 };
324
325 // macOS document shortcuts: https://support.apple.com/en-us/HT201236.
326 // The macOS shortcuts uses different word/line modifiers than most other
327 // platforms.
328 static final Map<ShortcutActivator, Intent> _macShortcuts = <ShortcutActivator, Intent>{
329 for (final bool pressShift in const <bool>[true, false])
330 ...<SingleActivator, Intent>{
331 SingleActivator(LogicalKeyboardKey.backspace, shift: pressShift): const DeleteCharacterIntent(forward: false),
332 SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: pressShift): const DeleteToNextWordBoundaryIntent(forward: false),
333 SingleActivator(LogicalKeyboardKey.backspace, meta: true, shift: pressShift): const DeleteToLineBreakIntent(forward: false),
334 SingleActivator(LogicalKeyboardKey.delete, shift: pressShift): const DeleteCharacterIntent(forward: true),
335 SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: pressShift): const DeleteToNextWordBoundaryIntent(forward: true),
336 SingleActivator(LogicalKeyboardKey.delete, meta: true, shift: pressShift): const DeleteToLineBreakIntent(forward: true),
337 },
338
339 const SingleActivator(LogicalKeyboardKey.arrowLeft): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
340 const SingleActivator(LogicalKeyboardKey.arrowRight): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
341 const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
342 const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
343
344 // Shift + Arrow: Extend selection.
345 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
346 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
347 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
348 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
349
350 const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
351 const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
352 const SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
353 const SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
354
355 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
356 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
357 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: false),
358 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: true),
359
360 const SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
361 const SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
362 const SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
363 const SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
364
365 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): const ExpandSelectionToLineBreakIntent(forward: false),
366 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): const ExpandSelectionToLineBreakIntent(forward: true),
367 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
368 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
369
370 const SingleActivator(LogicalKeyboardKey.keyT, control: true): const TransposeCharactersIntent(),
371
372 const SingleActivator(LogicalKeyboardKey.home): const ScrollToDocumentBoundaryIntent(forward: false),
373 const SingleActivator(LogicalKeyboardKey.end): const ScrollToDocumentBoundaryIntent(forward: true),
374 const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
375 const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
376
377 const SingleActivator(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
378 const SingleActivator(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
379 const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
380 const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
381
382 const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
383 const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
384 const SingleActivator(LogicalKeyboardKey.keyV, meta: true): const PasteTextIntent(SelectionChangedCause.keyboard),
385 const SingleActivator(LogicalKeyboardKey.keyA, meta: true): const SelectAllTextIntent(SelectionChangedCause.keyboard),
386 const SingleActivator(LogicalKeyboardKey.keyZ, meta: true): const UndoTextIntent(SelectionChangedCause.keyboard),
387 const SingleActivator(LogicalKeyboardKey.keyZ, shift: true, meta: true): const RedoTextIntent(SelectionChangedCause.keyboard),
388 const SingleActivator(LogicalKeyboardKey.keyE, control: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
389 const SingleActivator(LogicalKeyboardKey.keyA, control: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
390 const SingleActivator(LogicalKeyboardKey.keyF, control: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
391 const SingleActivator(LogicalKeyboardKey.keyB, control: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
392 const SingleActivator(LogicalKeyboardKey.keyN, control: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
393 const SingleActivator(LogicalKeyboardKey.keyP, control: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
394 // These keys should go to the IME when a field is focused, not to other
395 // Shortcuts.
396 const SingleActivator(LogicalKeyboardKey.space): const DoNothingAndStopPropagationTextIntent(),
397 const SingleActivator(LogicalKeyboardKey.enter): const DoNothingAndStopPropagationTextIntent(),
398 // The following key combinations have no effect on text editing on this
399 // platform:
400 // * End
401 // * Home
402 // * Control + shift? + end
403 // * Control + shift? + home
404 // * Control + shift? + Z
405 };
406
407 // There is no complete documentation of iOS shortcuts: use macOS ones.
408 static final Map<ShortcutActivator, Intent> _iOSShortcuts = _macShortcuts;
409
410 // The following key combinations have no effect on text editing on this
411 // platform:
412 // * Meta + X
413 // * Meta + C
414 // * Meta + V
415 // * Meta + A
416 // * Meta + shift? + arrow down
417 // * Meta + shift? + arrow left
418 // * Meta + shift? + arrow right
419 // * Meta + shift? + arrow up
420 // * Meta + delete
421 // * Meta + backspace
422 static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
423 ..._commonShortcuts,
424 const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
425 const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
426 const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
427 const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true, continuesAtWrap: true),
428 const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
429 const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, continuesAtWrap: true),
430 const SingleActivator(LogicalKeyboardKey.home, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
431 const SingleActivator(LogicalKeyboardKey.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
432 const SingleActivator(LogicalKeyboardKey.home, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
433 const SingleActivator(LogicalKeyboardKey.end, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
434 };
435
436 // Web handles its text selection natively and doesn't use any of these
437 // shortcuts in Flutter.
438 static final Map<ShortcutActivator, Intent> _webDisablingTextShortcuts = <ShortcutActivator, Intent>{
439 for (final bool pressShift in const <bool>[true, false])
440 ...<SingleActivator, Intent>{
441 SingleActivator(LogicalKeyboardKey.backspace, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
442 SingleActivator(LogicalKeyboardKey.delete, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
443 SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
444 SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
445 SingleActivator(LogicalKeyboardKey.backspace, control: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
446 SingleActivator(LogicalKeyboardKey.delete, control: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
447 SingleActivator(LogicalKeyboardKey.backspace, meta: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
448 SingleActivator(LogicalKeyboardKey.delete, meta: true, shift: pressShift): const DoNothingAndStopPropagationTextIntent(),
449 },
450 ..._commonDisablingTextShortcuts,
451 const SingleActivator(LogicalKeyboardKey.keyX, control: true): const DoNothingAndStopPropagationTextIntent(),
452 const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const DoNothingAndStopPropagationTextIntent(),
453 const SingleActivator(LogicalKeyboardKey.keyC, control: true): const DoNothingAndStopPropagationTextIntent(),
454 const SingleActivator(LogicalKeyboardKey.keyC, meta: true): const DoNothingAndStopPropagationTextIntent(),
455 const SingleActivator(LogicalKeyboardKey.keyV, control: true): const DoNothingAndStopPropagationTextIntent(),
456 const SingleActivator(LogicalKeyboardKey.keyV, meta: true): const DoNothingAndStopPropagationTextIntent(),
457 const SingleActivator(LogicalKeyboardKey.keyA, control: true): const DoNothingAndStopPropagationTextIntent(),
458 const SingleActivator(LogicalKeyboardKey.keyA, meta: true): const DoNothingAndStopPropagationTextIntent(),
459 };
460
461 static const Map<ShortcutActivator, Intent> _commonDisablingTextShortcuts = <ShortcutActivator, Intent>{
462 SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): DoNothingAndStopPropagationTextIntent(),
463 SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): DoNothingAndStopPropagationTextIntent(),
464 SingleActivator(LogicalKeyboardKey.arrowRight, alt: true): DoNothingAndStopPropagationTextIntent(),
465 SingleActivator(LogicalKeyboardKey.arrowUp, alt: true): DoNothingAndStopPropagationTextIntent(),
466 SingleActivator(LogicalKeyboardKey.arrowDown, meta: true): DoNothingAndStopPropagationTextIntent(),
467 SingleActivator(LogicalKeyboardKey.arrowLeft, meta: true): DoNothingAndStopPropagationTextIntent(),
468 SingleActivator(LogicalKeyboardKey.arrowRight, meta: true): DoNothingAndStopPropagationTextIntent(),
469 SingleActivator(LogicalKeyboardKey.arrowUp, meta: true): DoNothingAndStopPropagationTextIntent(),
470 SingleActivator(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationTextIntent(),
471 SingleActivator(LogicalKeyboardKey.arrowLeft): DoNothingAndStopPropagationTextIntent(),
472 SingleActivator(LogicalKeyboardKey.arrowRight): DoNothingAndStopPropagationTextIntent(),
473 SingleActivator(LogicalKeyboardKey.arrowUp): DoNothingAndStopPropagationTextIntent(),
474 SingleActivator(LogicalKeyboardKey.arrowLeft, control: true): DoNothingAndStopPropagationTextIntent(),
475 SingleActivator(LogicalKeyboardKey.arrowRight, control: true): DoNothingAndStopPropagationTextIntent(),
476 SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
477 SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
478 SingleActivator(LogicalKeyboardKey.space): DoNothingAndStopPropagationTextIntent(),
479 SingleActivator(LogicalKeyboardKey.enter): DoNothingAndStopPropagationTextIntent(),
480 };
481
482 static final Map<ShortcutActivator, Intent> _macDisablingTextShortcuts = <ShortcutActivator, Intent>{
483 ..._commonDisablingTextShortcuts,
484 ..._iOSDisablingTextShortcuts,
485 const SingleActivator(LogicalKeyboardKey.escape): const DoNothingAndStopPropagationTextIntent(),
486 const SingleActivator(LogicalKeyboardKey.tab): const DoNothingAndStopPropagationTextIntent(),
487 const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingAndStopPropagationTextIntent(),
488 const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
489 const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
490 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const DoNothingAndStopPropagationTextIntent(),
491 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const DoNothingAndStopPropagationTextIntent(),
492 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
493 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
494 const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, meta: true): const DoNothingAndStopPropagationTextIntent(),
495 const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, meta: true): const DoNothingAndStopPropagationTextIntent(),
496 const SingleActivator(LogicalKeyboardKey.pageUp): const DoNothingAndStopPropagationTextIntent(),
497 const SingleActivator(LogicalKeyboardKey.pageDown): const DoNothingAndStopPropagationTextIntent(),
498 const SingleActivator(LogicalKeyboardKey.end): const DoNothingAndStopPropagationTextIntent(),
499 const SingleActivator(LogicalKeyboardKey.home): const DoNothingAndStopPropagationTextIntent(),
500 const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const DoNothingAndStopPropagationTextIntent(),
501 const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const DoNothingAndStopPropagationTextIntent(),
502 const SingleActivator(LogicalKeyboardKey.end, shift: true): const DoNothingAndStopPropagationTextIntent(),
503 const SingleActivator(LogicalKeyboardKey.home, shift: true): const DoNothingAndStopPropagationTextIntent(),
504 const SingleActivator(LogicalKeyboardKey.end, control: true): const DoNothingAndStopPropagationTextIntent(),
505 const SingleActivator(LogicalKeyboardKey.home, control: true): const DoNothingAndStopPropagationTextIntent(),
506 };
507
508 // Hand backspace/delete events that do not depend on text layout (delete
509 // character and delete to the next word) back to the IME to allow it to
510 // update composing text properly.
511 static const Map<ShortcutActivator, Intent> _iOSDisablingTextShortcuts = <ShortcutActivator, Intent>{
512 SingleActivator(LogicalKeyboardKey.backspace): DoNothingAndStopPropagationTextIntent(),
513 SingleActivator(LogicalKeyboardKey.backspace, shift: true): DoNothingAndStopPropagationTextIntent(),
514 SingleActivator(LogicalKeyboardKey.delete): DoNothingAndStopPropagationTextIntent(),
515 SingleActivator(LogicalKeyboardKey.delete, shift: true): DoNothingAndStopPropagationTextIntent(),
516 SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
517 SingleActivator(LogicalKeyboardKey.backspace, alt: true): DoNothingAndStopPropagationTextIntent(),
518 SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
519 SingleActivator(LogicalKeyboardKey.delete, alt: true): DoNothingAndStopPropagationTextIntent(),
520 };
521
522 static Map<ShortcutActivator, Intent> get _shortcuts {
523 return switch (defaultTargetPlatform) {
524 TargetPlatform.android => _androidShortcuts,
525 TargetPlatform.fuchsia => _fuchsiaShortcuts,
526 TargetPlatform.iOS => _iOSShortcuts,
527 TargetPlatform.linux => _linuxShortcuts,
528 TargetPlatform.macOS => _macShortcuts,
529 TargetPlatform.windows => _windowsShortcuts,
530 };
531 }
532
533 Map<ShortcutActivator, Intent>? _getDisablingShortcut() {
534 if (kIsWeb) {
535 switch (defaultTargetPlatform) {
536 case TargetPlatform.linux:
537 return <ShortcutActivator, Intent>{
538 ..._webDisablingTextShortcuts,
539 for (final ShortcutActivator activator in _linuxNumpadShortcuts.keys)
540 activator as SingleActivator: const DoNothingAndStopPropagationTextIntent(),
541 };
542 case TargetPlatform.android:
543 case TargetPlatform.fuchsia:
544 case TargetPlatform.windows:
545 case TargetPlatform.iOS:
546 case TargetPlatform.macOS:
547 return _webDisablingTextShortcuts;
548 }
549 }
550 switch (defaultTargetPlatform) {
551 case TargetPlatform.android:
552 case TargetPlatform.fuchsia:
553 case TargetPlatform.linux:
554 case TargetPlatform.windows:
555 return null;
556 case TargetPlatform.iOS:
557 return _iOSDisablingTextShortcuts;
558 case TargetPlatform.macOS:
559 return _macDisablingTextShortcuts;
560 }
561 }
562
563 @override
564 Widget build(BuildContext context) {
565 Widget result = child;
566 final Map<ShortcutActivator, Intent>? disablingShortcut = _getDisablingShortcut();
567 if (disablingShortcut != null) {
568 // These shortcuts make sure of the following:
569 //
570 // 1. Shortcuts fired when an EditableText is focused are ignored and
571 // forwarded to the platform by the EditableText's Actions, because it
572 // maps DoNothingAndStopPropagationTextIntent to DoNothingAction.
573 // 2. Shortcuts fired when no EditableText is focused will still trigger
574 // _shortcuts assuming DoNothingAndStopPropagationTextIntent is
575 // unhandled elsewhere.
576 result = Shortcuts(
577 debugLabel: '<Web Disabling Text Editing Shortcuts>',
578 shortcuts: disablingShortcut,
579 child: result
580 );
581 }
582 return Shortcuts(
583 debugLabel: '<Default Text Editing Shortcuts>',
584 shortcuts: _shortcuts,
585 child: result
586 );
587 }
588}
589
590/// Maps the selector from NSStandardKeyBindingResponding to the Intent if the
591/// selector is recognized.
592Intent? intentForMacOSSelector(String selectorName) {
593 const Map<String, Intent> selectorToIntent = <String, Intent>{
594 'deleteBackward:': DeleteCharacterIntent(forward: false),
595 'deleteWordBackward:': DeleteToNextWordBoundaryIntent(forward: false),
596 'deleteToBeginningOfLine:': DeleteToLineBreakIntent(forward: false),
597 'deleteForward:': DeleteCharacterIntent(forward: true),
598 'deleteWordForward:': DeleteToNextWordBoundaryIntent(forward: true),
599 'deleteToEndOfLine:': DeleteToLineBreakIntent(forward: true),
600
601 'moveLeft:': ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
602 'moveRight:': ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
603 'moveForward:': ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
604 'moveBackward:': ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
605
606 'moveUp:': ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
607 'moveDown:': ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
608
609 'moveLeftAndModifySelection:': ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
610 'moveRightAndModifySelection:': ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
611 'moveUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
612 'moveDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
613
614 'moveWordLeft:': ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
615 'moveWordRight:': ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
616 'moveToBeginningOfParagraph:': ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
617 'moveToEndOfParagraph:': ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
618
619 'moveWordLeftAndModifySelection:': ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: false),
620 'moveWordRightAndModifySelection:': ExtendSelectionToNextWordBoundaryOrCaretLocationIntent(forward: true),
621 'moveParagraphBackwardAndModifySelection:': ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: false),
622 'moveParagraphForwardAndModifySelection:': ExtendSelectionToNextParagraphBoundaryOrCaretLocationIntent(forward: true),
623
624 'moveToLeftEndOfLine:': ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
625 'moveToRightEndOfLine:': ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
626 'moveToBeginningOfDocument:': ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
627 'moveToEndOfDocument:': ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
628
629 'moveToLeftEndOfLineAndModifySelection:': ExpandSelectionToLineBreakIntent(forward: false),
630 'moveToRightEndOfLineAndModifySelection:': ExpandSelectionToLineBreakIntent(forward: true),
631 'moveToBeginningOfDocumentAndModifySelection:': ExpandSelectionToDocumentBoundaryIntent(forward: false),
632 'moveToEndOfDocumentAndModifySelection:': ExpandSelectionToDocumentBoundaryIntent(forward: true),
633
634 'transpose:': TransposeCharactersIntent(),
635
636 'scrollToBeginningOfDocument:': ScrollToDocumentBoundaryIntent(forward: false),
637 'scrollToEndOfDocument:': ScrollToDocumentBoundaryIntent(forward: true),
638
639 'scrollPageUp:': ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
640 'scrollPageDown:': ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
641 'pageUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
642 'pageDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
643
644 // Escape key when there's no IME selection popup.
645 'cancelOperation:': DismissIntent(),
646 // Tab when there's no IME selection.
647 'insertTab:': NextFocusIntent(),
648 'insertBacktab:': PreviousFocusIntent(),
649 };
650 return selectorToIntent[selectorName];
651}
652