Skip to content

Commit 053ed6d

Browse files
committed
feat(android): naitve text process actions (WIP)
1 parent b3beaea commit 053ed6d

File tree

3 files changed

+115
-2
lines changed

3 files changed

+115
-2
lines changed

example/lib/main.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ class _HomePageState extends State<HomePage> {
166166
),
167167
TimeStampEmbedBuilder(),
168168
],
169+
displayNativeContextMenuItems: true,
169170
),
170171
),
171172
),

lib/src/editor/config/editor_config.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class QuillEditorConfig {
8181
this.readOnlyMouseCursor = SystemMouseCursors.text,
8282
this.onPerformAction,
8383
@experimental this.customLeadingBlockBuilder,
84+
this.displayNativeContextMenuItems = false,
8485
});
8586

8687
@experimental
@@ -452,6 +453,32 @@ class QuillEditorConfig {
452453
/// Called when a text input action is performed.
453454
final void Function(TextInputAction action)? onPerformAction;
454455

456+
/// The native context menu items like `Translate` and `Search` on Android.
457+
///
458+
/// This feature is platform-specific and will
459+
/// be silently ignored on platforms other than Android.
460+
///
461+
/// To use this feature, ensure the following is added in your `AndroidManifest.xml`:
462+
///
463+
/// ```xml
464+
/// <queries>
465+
/// <intent>
466+
/// <action android:name="android.intent.action.PROCESS_TEXT"/>
467+
/// <data android:mimeType="text/plain"/>
468+
/// </intent>
469+
/// </queries>
470+
/// ```
471+
///
472+
/// This is the case for newly created Flutter projects.
473+
///
474+
/// If the 'queries' element is not found, this config will be ignored.
475+
/// For more details, refer to [DefaultProcessTextService](https://api.flutter.dev/flutter/services/DefaultProcessTextService-class.html).
476+
///
477+
/// This is always ignored when [contextMenuBuilder] is not null.
478+
///
479+
/// Defaults to `false`.
480+
final bool displayNativeContextMenuItems;
481+
455482
// IMPORTANT For project authors: The copyWith()
456483
// should be manually updated each time we add or remove a property
457484

@@ -512,6 +539,7 @@ class QuillEditorConfig {
512539
void Function()? onScribbleActivated,
513540
EdgeInsets? scribbleAreaInsets,
514541
void Function(TextInputAction action)? onPerformAction,
542+
bool? displayNativeContextMenuItems,
515543
}) {
516544
return QuillEditorConfig(
517545
customLeadingBlockBuilder:
@@ -581,6 +609,8 @@ class QuillEditorConfig {
581609
onScribbleActivated: onScribbleActivated ?? this.onScribbleActivated,
582610
scribbleAreaInsets: scribbleAreaInsets ?? this.scribbleAreaInsets,
583611
onPerformAction: onPerformAction ?? this.onPerformAction,
612+
displayNativeContextMenuItems:
613+
displayNativeContextMenuItems ?? this.displayNativeContextMenuItems,
584614
);
585615
}
586616
}

lib/src/editor/editor.dart

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
88
import 'package:flutter/rendering.dart';
99
import 'package:flutter/scheduler.dart';
1010
import 'package:flutter/services.dart';
11+
import 'package:meta/meta.dart';
1112

1213
import '../common/utils/platform.dart';
1314
import '../controller/quill_controller.dart';
@@ -19,6 +20,7 @@ import 'config/editor_config.dart';
1920
import 'embed/embed_editor_builder.dart';
2021
import 'raw_editor/config/raw_editor_config.dart';
2122
import 'raw_editor/raw_editor.dart';
23+
import 'raw_editor/raw_editor_state.dart';
2224
import 'widgets/box.dart';
2325
import 'widgets/cursor.dart';
2426
import 'widgets/delegate.dart';
@@ -196,6 +198,71 @@ class QuillEditorState extends State<QuillEditor>
196198
QuillEditorConfig get configurations => widget.config;
197199
QuillEditorConfig get config => widget.config;
198200

201+
/// The native context menu items (e.g., `Translate`, `Search`).
202+
/// This is Android-specific and is always `null` on other platforms.
203+
List<ProcessTextAction>? _nativeTextProcessActions;
204+
205+
// Always `null` on platforms other than Android.
206+
@visibleForTesting
207+
@internal
208+
ProcessTextService? processTextService;
209+
210+
/// Query the engine to initialize the list of text processing actions to show
211+
/// in the text selection toolbar on Android.
212+
Future<void> _initAndroidNativeTextProcessActions() async {
213+
if (isAndroidApp && config.displayNativeContextMenuItems) {
214+
processTextService ??= DefaultProcessTextService();
215+
_nativeTextProcessActions = [
216+
...await processTextService!.queryTextActions()
217+
];
218+
}
219+
}
220+
221+
// For the original method, refer to: https://github.com/flutter/flutter/blob/9e211cabbd72de59d79decacfe0ad6f707c61366/packages/flutter/lib/src/widgets/editable_text.dart#L3059-L3091
222+
List<ContextMenuButtonItem> _buildTextProcessingActionButtonItems(
223+
QuillRawEditorState rawEditorState,
224+
) {
225+
final buttonItems = <ContextMenuButtonItem>[];
226+
227+
final textEditingValue = controller.plainTextEditingValue;
228+
final selection = textEditingValue.selection;
229+
if (!selection.isValid || selection.isCollapsed) {
230+
return buttonItems;
231+
}
232+
233+
for (final action in _nativeTextProcessActions ?? []) {
234+
buttonItems.add(
235+
ContextMenuButtonItem(
236+
label: action.label,
237+
onPressed: () async {
238+
final selectedText =
239+
controller.selection.textInside(textEditingValue.text);
240+
if (selectedText.isEmpty) {
241+
return;
242+
}
243+
244+
final processedText = await processTextService!.processTextAction(
245+
action.id,
246+
selectedText,
247+
controller.readOnly,
248+
);
249+
250+
// If an activity does not return a modified version, just hide the toolbar.
251+
// Otherwise use the result to replace the selected text.
252+
final allowPaste =
253+
!controller.readOnly && textEditingValue.selection.isValid;
254+
if (processedText != null && allowPaste) {
255+
// TODO: Paste the processedText
256+
} else {
257+
rawEditorState.hideToolbar();
258+
}
259+
},
260+
),
261+
);
262+
}
263+
return buttonItems;
264+
}
265+
199266
@override
200267
void initState() {
201268
super.initState();
@@ -218,6 +285,22 @@ class QuillEditorState extends State<QuillEditor>
218285
_editorKey.currentState?.hideToolbar();
219286
}
220287
});
288+
_initAndroidNativeTextProcessActions();
289+
}
290+
291+
Widget _defaultContextMenuBuilder(
292+
BuildContext context,
293+
QuillRawEditorState rawEditorState,
294+
) {
295+
return TextFieldTapRegion(
296+
child: AdaptiveTextSelectionToolbar.buttonItems(
297+
buttonItems: [
298+
...rawEditorState.contextMenuButtonItems,
299+
..._buildTextProcessingActionButtonItems(rawEditorState)
300+
],
301+
anchors: rawEditorState.contextMenuAnchors,
302+
),
303+
);
221304
}
222305

223306
@override
@@ -277,8 +360,7 @@ class QuillEditorState extends State<QuillEditor>
277360
placeholder: config.placeholder,
278361
onLaunchUrl: config.onLaunchUrl,
279362
contextMenuBuilder: showSelectionToolbar
280-
? (config.contextMenuBuilder ??
281-
QuillRawEditorConfig.defaultContextMenuBuilder)
363+
? config.contextMenuBuilder ?? _defaultContextMenuBuilder
282364
: null,
283365
showSelectionHandles: isMobile,
284366
showCursor: config.showCursor ?? true,

0 commit comments

Comments
 (0)