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