Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b3f9bdc

Browse files
committed
[ios_edit_menu]add basic menu with default actions
use targetRect API to auto determine the arrow direction parse global rects add unit tests add checks for copy/cut/delete use platform channel instead of text input channel, and use camel case add supportsShowingSystemContextMenu flag format add hide menu functionality add onDismiss callback use platform channel instead, and rename method fix conflict rename ContextMenu.onDismissSystemContextMenu add comment
1 parent a9a6f59 commit b3f9bdc

14 files changed

+240
-3
lines changed

lib/ui/platform_dispatcher.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,14 @@ class PlatformDispatcher {
10751075
bool get nativeSpellCheckServiceDefined => _nativeSpellCheckServiceDefined;
10761076
bool _nativeSpellCheckServiceDefined = false;
10771077

1078+
/// Whether showing system context menu is supported on the current platform.
1079+
///
1080+
/// This option is used by [AdaptiveTextSelectionToolbar] to decide whether
1081+
/// to show system context menu, or to fallback to the default Flutter context
1082+
/// menu.
1083+
bool get supportsShowingSystemContextMenu => _supportsShowingSystemContextMenu;
1084+
bool _supportsShowingSystemContextMenu = false;
1085+
10781086
/// Whether briefly displaying the characters as you type in obscured text
10791087
/// fields is enabled in system settings.
10801088
///
@@ -1142,6 +1150,14 @@ class PlatformDispatcher {
11421150
} else {
11431151
_nativeSpellCheckServiceDefined = false;
11441152
}
1153+
1154+
final bool? supportsShowingSystemContextMenu = data['supportsShowingSystemContextMenu'] as bool?;
1155+
if (supportsShowingSystemContextMenu != null) {
1156+
_supportsShowingSystemContextMenu = supportsShowingSystemContextMenu;
1157+
} else {
1158+
_supportsShowingSystemContextMenu = false;
1159+
}
1160+
11451161
// This field is optional.
11461162
final bool? brieflyShowPassword = data['brieflyShowPassword'] as bool?;
11471163
if (brieflyShowPassword != null) {

lib/ui/window.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,13 @@ class SingletonFlutterWindow extends FlutterView {
561561
/// service is specified.
562562
bool get nativeSpellCheckServiceDefined => platformDispatcher.nativeSpellCheckServiceDefined;
563563

564+
/// Whether the spell check service is supported on the current platform.
565+
///
566+
/// This option is used by [EditableTextState] to define its
567+
/// [SpellCheckConfiguration] when a default spell check service
568+
/// is requested.
569+
bool get supportsShowingSystemContextMenu => platformDispatcher.supportsShowingSystemContextMenu;
570+
564571
/// Whether briefly displaying the characters as you type in obscured text
565572
/// fields is enabled in system settings.
566573
///

lib/web_ui/lib/platform_dispatcher.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ abstract class PlatformDispatcher {
124124

125125
bool get nativeSpellCheckServiceDefined => false;
126126

127+
bool get supportsShowingSystemContextMenu => false;
128+
127129
bool get brieflyShowPassword => true;
128130

129131
VoidCallback? get onTextScaleFactorChanged;

lib/web_ui/lib/src/engine/window.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto
390390
@override
391391
bool get nativeSpellCheckServiceDefined => platformDispatcher.nativeSpellCheckServiceDefined;
392392

393+
@override
394+
bool get supportsShowingSystemContextMenu => platformDispatcher.supportsShowingSystemContextMenu;
395+
393396
@override
394397
bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword;
395398

lib/web_ui/lib/window.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ abstract class SingletonFlutterWindow extends FlutterView {
4646

4747
bool get nativeSpellCheckServiceDefined;
4848

49+
bool get supportsShowingSystemContextMenu;
50+
4951
bool get brieflyShowPassword;
5052

5153
bool get alwaysUse24HourFormat;

shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,12 @@ - (void)flutterTextInputView:(FlutterTextInputView*)textInputView
10821082
arguments:@[ @(client), @(start), @(end) ]];
10831083
}
10841084

1085+
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
1086+
willDismissEditMenuWithTextInputClient:(int)client {
1087+
[_platformChannel.get() invokeMethod:@"ContextMenu.onDismissSystemContextMenu"
1088+
arguments:@[ @(client) ]];
1089+
}
1090+
10851091
#pragma mark - FlutterViewEngineDelegate
10861092

10871093
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView showToolbar:(int)client {

shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.mm

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,11 +149,34 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
149149
} else if ([method isEqualToString:@"Share.invoke"]) {
150150
[self showShareViewController:args];
151151
result(nil);
152+
} else if ([method isEqualToString:@"ContextMenu.showSystemContextMenu"]) {
153+
[self showSystemContextMenu:args];
154+
result(nil);
155+
} else if ([method isEqualToString:@"ContextMenu.hideSystemContextMenu"]) {
156+
[self hideSystemContextMenu];
157+
result(nil);
152158
} else {
153159
result(FlutterMethodNotImplemented);
154160
}
155161
}
156162

163+
- (void)showSystemContextMenu:(NSDictionary*)args {
164+
// Right now only text inputs support system context menu.
165+
// However, it's possible to support it for non-text inputs too in the future.
166+
// See: https://github.com/flutter/flutter/issues/143033
167+
if (@available(iOS 16.0, *)) {
168+
FlutterTextInputPlugin* textInputPlugin = [_engine.get() textInputPlugin];
169+
[textInputPlugin showEditMenu:args];
170+
}
171+
}
172+
173+
- (void)hideSystemContextMenu {
174+
if (@available(iOS 16.0, *)) {
175+
FlutterTextInputPlugin* textInputPlugin = [_engine.get() textInputPlugin];
176+
[textInputPlugin hideEditMenu];
177+
}
178+
}
179+
157180
- (void)showShareViewController:(NSString*)content {
158181
UIViewController* engineViewController = [_engine.get() viewController];
159182

shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ typedef NS_ENUM(NSInteger, FlutterFloatingCursorDragState) {
6565
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView removeTextPlaceholder:(int)client;
6666
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
6767
didResignFirstResponderWithTextInputClient:(int)client;
68+
- (void)flutterTextInputView:(FlutterTextInputView*)textInputView
69+
willDismissEditMenuWithTextInputClient:(int)client;
6870
@end
6971

7072
#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERTEXTINPUTDELEGATE_H_

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ typedef NS_ENUM(NSInteger, FlutterScribbleInteractionStatus) {
6060
*/
6161
- (void)setUpIndirectScribbleInteraction:(id<FlutterViewResponder>)viewResponder;
6262
- (void)resetViewResponder;
63+
- (void)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0));
64+
- (void)hideEditMenu API_AVAILABLE(ios(16.0));
6365

6466
@end
6567

@@ -128,7 +130,8 @@ API_AVAILABLE(ios(13.0)) @interface FlutterTextPlaceholder : UITextPlaceholder
128130
#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
129131
FLUTTER_DARWIN_EXPORT
130132
#endif
131-
@interface FlutterTextInputView : UIView <UITextInput, UIScribbleInteractionDelegate>
133+
@interface FlutterTextInputView
134+
: UIView <UITextInput, UIScribbleInteractionDelegate, UIEditMenuInteractionDelegate>
132135

133136
// UITextInput
134137
@property(nonatomic, readonly) NSMutableString* text;
@@ -158,6 +161,8 @@ FLUTTER_DARWIN_EXPORT
158161
@property(nonatomic, weak) id<FlutterViewResponder> viewResponder;
159162
@property(nonatomic) FlutterScribbleFocusStatus scribbleFocusStatus;
160163
@property(nonatomic, strong) NSArray<FlutterTextSelectionRect*>* selectionRects;
164+
165+
@property(nonatomic, strong) UIEditMenuInteraction* editMenuInteraction API_AVAILABLE(ios(16.0));
161166
- (void)resetScribbleInteractionStatusIfEnding;
162167
- (BOOL)isScribbleAvailable;
163168

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,6 +794,7 @@ @interface FlutterTextInputView ()
794794
// This is cleared at the start of each keyboard interaction. (Enter a character, delete a character
795795
// etc)
796796
@property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter;
797+
@property(nonatomic, assign) CGRect editMenuTargetRect;
797798

798799
- (void)setEditableTransform:(NSArray*)matrix;
799800
@end
@@ -859,9 +860,44 @@ - (instancetype)initWithOwner:(FlutterTextInputPlugin*)textInputPlugin {
859860
}
860861
}
861862

863+
if (@available(iOS 16.0, *)) {
864+
_editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
865+
[self addInteraction:_editMenuInteraction];
866+
}
867+
862868
return self;
863869
}
864870

871+
- (UIMenu*)editMenuInteraction:(UIEditMenuInteraction*)interaction
872+
menuForConfiguration:(UIEditMenuConfiguration*)configuration
873+
suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions API_AVAILABLE(ios(16.0)) {
874+
return [UIMenu menuWithChildren:suggestedActions];
875+
}
876+
877+
- (void)editMenuInteraction:(UIEditMenuInteraction*)interaction
878+
willDismissMenuForConfiguration:(UIEditMenuConfiguration*)configuration
879+
animator:(id<UIEditMenuInteractionAnimating>)animator
880+
API_AVAILABLE(ios(16.0)) {
881+
[self.textInputDelegate flutterTextInputView:self
882+
willDismissEditMenuWithTextInputClient:_textInputClient];
883+
}
884+
885+
- (CGRect)editMenuInteraction:(UIEditMenuInteraction*)interaction
886+
targetRectForConfiguration:(UIEditMenuConfiguration*)configuration API_AVAILABLE(ios(16.0)) {
887+
return _editMenuTargetRect;
888+
}
889+
890+
- (void)showEditMenuWithTargetRect:(CGRect)targetRect API_AVAILABLE(ios(16.0)) {
891+
_editMenuTargetRect = targetRect;
892+
UIEditMenuConfiguration* config =
893+
[UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:CGPointZero];
894+
[self.editMenuInteraction presentEditMenuWithConfiguration:config];
895+
}
896+
897+
- (void)hideEditMenu API_AVAILABLE(ios(16.0)) {
898+
[self.editMenuInteraction dismissMenu];
899+
}
900+
865901
- (void)configureWithDictionary:(NSDictionary*)configuration {
866902
NSDictionary* inputType = configuration[kKeyboardType];
867903
NSString* keyboardAppearance = configuration[kKeyboardAppearance];
@@ -1148,8 +1184,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
11481184
if (action == @selector(paste:)) {
11491185
// Forbid pasting images, memojis, or other non-string content.
11501186
return [UIPasteboard generalPasteboard].hasStrings;
1187+
} else if (action == @selector(copy:) || action == @selector(cut:) ||
1188+
action == @selector(delete:)) {
1189+
return [self textInRange:_selectedTextRange].length > 0;
11511190
}
1152-
11531191
return [super canPerformAction:action withSender:sender];
11541192
}
11551193

@@ -2511,6 +2549,19 @@ - (void)takeKeyboardScreenshotAndDisplay {
25112549
_keyboardViewContainer.frame = _keyboardRect;
25122550
}
25132551

2552+
- (void)showEditMenu:(NSDictionary*)args API_AVAILABLE(ios(16.0)) {
2553+
NSDictionary<NSString*, NSNumber*>* encodedTargetRect = args[@"targetRect"];
2554+
CGRect globalTargetRect = CGRectMake(
2555+
[encodedTargetRect[@"x"] doubleValue], [encodedTargetRect[@"y"] doubleValue],
2556+
[encodedTargetRect[@"width"] doubleValue], [encodedTargetRect[@"height"] doubleValue]);
2557+
CGRect localTargetRect = [self.hostView convertRect:globalTargetRect toView:self.activeView];
2558+
[self.activeView showEditMenuWithTargetRect:localTargetRect];
2559+
}
2560+
2561+
- (void)hideEditMenu {
2562+
[self.activeView hideEditMenu];
2563+
}
2564+
25142565
- (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
25152566
NSArray* transform = dictionary[@"transform"];
25162567
[_activeView setEditableTransform:transform];

0 commit comments

Comments
 (0)