Skip to content

Commit e7d3391

Browse files
authored
fix(macos): Implement actions for ExpandSelectionToDocumentBoundaryIntent and ExpandSelectionToLineBreakIntent to use keyboard shortcuts, unrelated cleanup to the bug fix. (singerdmx#2279)
1 parent 0ac3188 commit e7d3391

File tree

6 files changed

+537
-422
lines changed

6 files changed

+537
-422
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
3+
import 'package:meta/meta.dart';
4+
5+
import '../../../common/utils/platform.dart';
6+
import '../../../document/attribute.dart';
7+
import 'editor_keyboard_shortcut_actions.dart';
8+
9+
final _isDesktopMacOS = isMacOS;
10+
11+
@internal
12+
Map<SingleActivator, Intent> defaultSinlgeActivatorIntents() {
13+
return {
14+
const SingleActivator(
15+
LogicalKeyboardKey.escape,
16+
): const HideSelectionToolbarIntent(),
17+
SingleActivator(
18+
LogicalKeyboardKey.keyZ,
19+
control: !_isDesktopMacOS,
20+
meta: _isDesktopMacOS,
21+
): const UndoTextIntent(SelectionChangedCause.keyboard),
22+
SingleActivator(
23+
LogicalKeyboardKey.keyY,
24+
control: !_isDesktopMacOS,
25+
meta: _isDesktopMacOS,
26+
): const RedoTextIntent(SelectionChangedCause.keyboard),
27+
28+
// Selection formatting.
29+
SingleActivator(
30+
LogicalKeyboardKey.keyB,
31+
control: !_isDesktopMacOS,
32+
meta: _isDesktopMacOS,
33+
): const ToggleTextStyleIntent(Attribute.bold),
34+
SingleActivator(
35+
LogicalKeyboardKey.keyU,
36+
control: !_isDesktopMacOS,
37+
meta: _isDesktopMacOS,
38+
): const ToggleTextStyleIntent(Attribute.underline),
39+
SingleActivator(
40+
LogicalKeyboardKey.keyI,
41+
control: !_isDesktopMacOS,
42+
meta: _isDesktopMacOS,
43+
): const ToggleTextStyleIntent(Attribute.italic),
44+
SingleActivator(
45+
LogicalKeyboardKey.keyS,
46+
control: !_isDesktopMacOS,
47+
meta: _isDesktopMacOS,
48+
shift: true,
49+
): const ToggleTextStyleIntent(Attribute.strikeThrough),
50+
SingleActivator(
51+
LogicalKeyboardKey.backquote,
52+
control: !_isDesktopMacOS,
53+
meta: _isDesktopMacOS,
54+
): const ToggleTextStyleIntent(Attribute.inlineCode),
55+
SingleActivator(
56+
LogicalKeyboardKey.tilde,
57+
control: !_isDesktopMacOS,
58+
meta: _isDesktopMacOS,
59+
shift: true,
60+
): const ToggleTextStyleIntent(Attribute.codeBlock),
61+
SingleActivator(
62+
LogicalKeyboardKey.keyB,
63+
control: !_isDesktopMacOS,
64+
meta: _isDesktopMacOS,
65+
shift: true,
66+
): const ToggleTextStyleIntent(Attribute.blockQuote),
67+
SingleActivator(
68+
LogicalKeyboardKey.keyK,
69+
control: !_isDesktopMacOS,
70+
meta: _isDesktopMacOS,
71+
): const QuillEditorApplyLinkIntent(),
72+
73+
// Lists
74+
SingleActivator(
75+
LogicalKeyboardKey.keyL,
76+
control: !_isDesktopMacOS,
77+
meta: _isDesktopMacOS,
78+
shift: true,
79+
): const ToggleTextStyleIntent(Attribute.ul),
80+
SingleActivator(
81+
LogicalKeyboardKey.keyO,
82+
control: !_isDesktopMacOS,
83+
meta: _isDesktopMacOS,
84+
shift: true,
85+
): const ToggleTextStyleIntent(Attribute.ol),
86+
SingleActivator(
87+
LogicalKeyboardKey.keyC,
88+
control: !_isDesktopMacOS,
89+
meta: _isDesktopMacOS,
90+
shift: true,
91+
): const QuillEditorApplyCheckListIntent(),
92+
93+
// Indents
94+
SingleActivator(
95+
LogicalKeyboardKey.keyM,
96+
control: !_isDesktopMacOS,
97+
meta: _isDesktopMacOS,
98+
): const IndentSelectionIntent(true),
99+
SingleActivator(
100+
LogicalKeyboardKey.keyM,
101+
control: !_isDesktopMacOS,
102+
meta: _isDesktopMacOS,
103+
shift: true,
104+
): const IndentSelectionIntent(false),
105+
106+
// Headers
107+
SingleActivator(
108+
LogicalKeyboardKey.digit1,
109+
control: !_isDesktopMacOS,
110+
meta: _isDesktopMacOS,
111+
): const QuillEditorApplyHeaderIntent(Attribute.h1),
112+
SingleActivator(
113+
LogicalKeyboardKey.digit2,
114+
control: !_isDesktopMacOS,
115+
meta: _isDesktopMacOS,
116+
): const QuillEditorApplyHeaderIntent(Attribute.h2),
117+
SingleActivator(
118+
LogicalKeyboardKey.digit3,
119+
control: !_isDesktopMacOS,
120+
meta: _isDesktopMacOS,
121+
): const QuillEditorApplyHeaderIntent(Attribute.h3),
122+
SingleActivator(
123+
LogicalKeyboardKey.digit4,
124+
control: !_isDesktopMacOS,
125+
meta: _isDesktopMacOS,
126+
): const QuillEditorApplyHeaderIntent(Attribute.h4),
127+
SingleActivator(
128+
LogicalKeyboardKey.digit5,
129+
control: !_isDesktopMacOS,
130+
meta: _isDesktopMacOS,
131+
): const QuillEditorApplyHeaderIntent(Attribute.h5),
132+
SingleActivator(
133+
LogicalKeyboardKey.digit6,
134+
control: !_isDesktopMacOS,
135+
meta: _isDesktopMacOS,
136+
): const QuillEditorApplyHeaderIntent(Attribute.h6),
137+
SingleActivator(
138+
LogicalKeyboardKey.digit0,
139+
control: !_isDesktopMacOS,
140+
meta: _isDesktopMacOS,
141+
): const QuillEditorApplyHeaderIntent(Attribute.header),
142+
143+
SingleActivator(
144+
LogicalKeyboardKey.keyG,
145+
control: !_isDesktopMacOS,
146+
meta: _isDesktopMacOS,
147+
): const QuillEditorInsertEmbedIntent(Attribute.image),
148+
149+
SingleActivator(
150+
LogicalKeyboardKey.keyF,
151+
control: !_isDesktopMacOS,
152+
meta: _isDesktopMacOS,
153+
): const OpenSearchIntent(),
154+
155+
// Arrow key scrolling
156+
SingleActivator(
157+
LogicalKeyboardKey.arrowUp,
158+
control: !_isDesktopMacOS,
159+
meta: _isDesktopMacOS,
160+
): const ScrollIntent(direction: AxisDirection.up),
161+
SingleActivator(
162+
LogicalKeyboardKey.arrowDown,
163+
control: !_isDesktopMacOS,
164+
meta: _isDesktopMacOS,
165+
): const ScrollIntent(direction: AxisDirection.down),
166+
SingleActivator(
167+
LogicalKeyboardKey.pageUp,
168+
control: !_isDesktopMacOS,
169+
meta: _isDesktopMacOS,
170+
): const ScrollIntent(
171+
direction: AxisDirection.up, type: ScrollIncrementType.page),
172+
SingleActivator(
173+
LogicalKeyboardKey.pageDown,
174+
control: !_isDesktopMacOS,
175+
meta: _isDesktopMacOS,
176+
): const ScrollIntent(
177+
direction: AxisDirection.down, type: ScrollIncrementType.page),
178+
};
179+
}

lib/src/editor/raw_editor/raw_editor_actions.dart renamed to lib/src/editor/raw_editor/keyboard_shortcuts/editor_keyboard_shortcut_actions.dart

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import 'package:flutter/material.dart';
22

3-
import '../../../translations.dart';
4-
import '../../document/attribute.dart';
5-
import '../../document/style.dart';
6-
import '../../toolbar/buttons/link_style2_button.dart';
7-
import '../../toolbar/buttons/search/search_dialog.dart';
8-
import '../editor.dart';
9-
import '../widgets/link.dart';
10-
import 'raw_editor_state.dart';
11-
import 'raw_editor_text_boundaries.dart';
3+
import '../../../../translations.dart';
4+
import '../../../document/attribute.dart';
5+
import '../../../document/style.dart';
6+
import '../../../toolbar/buttons/link_style2_button.dart';
7+
import '../../../toolbar/buttons/search/search_dialog.dart';
8+
import '../../editor.dart';
9+
import '../../widgets/link.dart';
10+
import '../raw_editor_state.dart';
11+
import '../raw_editor_text_boundaries.dart';
1212

1313
// ------------------------------- Text Actions -------------------------------
1414
class QuillEditorDeleteTextAction<T extends DirectionalTextEditingIntent>
@@ -268,6 +268,98 @@ class QuillEditorExtendSelectionOrCaretPositionAction extends ContextAction<
268268
state.textEditingValue.selection.isValid;
269269
}
270270

271+
/// Expands the selection to the start/end of the document.
272+
///
273+
/// This matches macOS behavior and differs from [ExpandSelectionToLineBreakIntent].
274+
///
275+
/// See: [ExpandSelectionToDocumentBoundaryIntent].
276+
class ExpandSelectionToDocumentBoundaryAction
277+
extends ContextAction<ExpandSelectionToDocumentBoundaryIntent> {
278+
ExpandSelectionToDocumentBoundaryAction(this.state);
279+
280+
final QuillRawEditorState state;
281+
282+
@override
283+
Object? invoke(ExpandSelectionToDocumentBoundaryIntent intent,
284+
[BuildContext? context]) {
285+
final currentSelection = state.controller.selection;
286+
final documentLength = state.controller.document.length;
287+
288+
final newSelection = intent.forward
289+
? currentSelection.copyWith(
290+
extentOffset: documentLength,
291+
)
292+
: currentSelection.copyWith(
293+
extentOffset: 0,
294+
);
295+
return Actions.invoke(
296+
context ?? (throw StateError('BuildContext should not be null.')),
297+
UpdateSelectionIntent(
298+
state.textEditingValue,
299+
newSelection,
300+
SelectionChangedCause.keyboard,
301+
),
302+
);
303+
}
304+
}
305+
306+
/// Extends the selection to the next/previous line break (`\n`).
307+
///
308+
/// This behavior is standard on macOS.
309+
///
310+
/// See: [ExpandSelectionToLineBreakIntent]
311+
class ExpandSelectionToLineBreakAction
312+
extends ContextAction<ExpandSelectionToLineBreakIntent> {
313+
ExpandSelectionToLineBreakAction(this.state);
314+
315+
final QuillRawEditorState state;
316+
@override
317+
Object? invoke(ExpandSelectionToLineBreakIntent intent,
318+
[BuildContext? context]) {
319+
// Plain text of the document (needed to find line breaks)
320+
final text = state.controller.plainTextEditingValue.text;
321+
322+
final currentSelection = state.controller.selection;
323+
324+
// Calculate the next or previous line break based on direction
325+
final searchStartOffset = currentSelection.extentOffset;
326+
327+
final targetLineBreak = () {
328+
if (intent.forward) {
329+
final nextLineBreak = text.indexOf('\n', searchStartOffset);
330+
final noNextLineBreak = nextLineBreak == -1;
331+
return noNextLineBreak ? text.length : nextLineBreak + 1;
332+
}
333+
334+
// Backward
335+
336+
// Ensure (searchStartOffset - 1) is not negative to avoid [RangeError]
337+
final safePreviousSearchOffset =
338+
(searchStartOffset > 0) ? (searchStartOffset - 1) : 0;
339+
340+
final previousLineBreak =
341+
text.lastIndexOf('\n', safePreviousSearchOffset);
342+
343+
final noPreviousLineBreak = previousLineBreak == -1;
344+
return noPreviousLineBreak ? 0 : previousLineBreak;
345+
}();
346+
347+
// Create a new selection, extending it to the line break was found
348+
final newSelection = currentSelection.copyWith(
349+
extentOffset: targetLineBreak,
350+
);
351+
352+
return Actions.invoke(
353+
context ?? (throw StateError('BuildContext should not be null.')),
354+
UpdateSelectionIntent(
355+
state.textEditingValue,
356+
newSelection,
357+
SelectionChangedCause.keyboard,
358+
),
359+
);
360+
}
361+
}
362+
271363
class QuillEditorUpdateTextSelectionToAdjacentLineAction<
272364
T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
273365
QuillEditorUpdateTextSelectionToAdjacentLineAction(this.state);
@@ -627,6 +719,7 @@ class QuillEditorInsertEmbedIntent extends Intent {
627719
final Attribute type;
628720
}
629721

722+
/// Navigate to the start or end of the document
630723
class NavigateToDocumentBoundaryAction
631724
extends ContextAction<ScrollToDocumentBoundaryIntent> {
632725
NavigateToDocumentBoundaryAction(this.state);

0 commit comments

Comments
 (0)