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

Commit cb1c312

Browse files
authored
Support right-clicking on iPadOS (#27019)
1 parent e3357d2 commit cb1c312

File tree

8 files changed

+114
-15
lines changed

8 files changed

+114
-15
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ shoryukenn <[email protected]>
1818
SOTEC GmbH & Co. KG <[email protected]>
1919
Hidenori Matsubayashi <[email protected]>
2020
Sarbagya Dhaubanjar <[email protected]>
21+
Callum Moffat <[email protected]>

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -814,7 +814,8 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state {
814814
// in the status bar area are available to framework code. The change type (optional) of the faked
815815
// touch is specified in the second argument.
816816
- (void)dispatchTouches:(NSSet*)touches
817-
pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change {
817+
pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
818+
event:(UIEvent*)event {
818819
if (!_engine) {
819820
return;
820821
}
@@ -925,31 +926,42 @@ - (void)dispatchTouches:(NSSet*)touches
925926
pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
926927
}
927928

929+
if (@available(iOS 13.4, *)) {
930+
if (event != nullptr) {
931+
pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0)
932+
? flutter::PointerButtonMouse::kPointerButtonMousePrimary
933+
: 0) |
934+
(((event.buttonMask & UIEventButtonMaskSecondary) > 0)
935+
? flutter::PointerButtonMouse::kPointerButtonMouseSecondary
936+
: 0);
937+
}
938+
}
939+
928940
packet->SetPointerData(pointer_index++, pointer_data);
929941
}
930942

931943
[_engine.get() dispatchPointerDataPacket:std::move(packet)];
932944
}
933945

934946
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
935-
[self dispatchTouches:touches pointerDataChangeOverride:nullptr];
947+
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
936948
}
937949

938950
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
939-
[self dispatchTouches:touches pointerDataChangeOverride:nullptr];
951+
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
940952
}
941953

942954
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
943-
[self dispatchTouches:touches pointerDataChangeOverride:nullptr];
955+
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
944956
}
945957

946958
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
947-
[self dispatchTouches:touches pointerDataChangeOverride:nullptr];
959+
[self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
948960
}
949961

950962
- (void)forceTouchesCancelled:(NSSet*)touches {
951963
flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
952-
[self dispatchTouches:touches pointerDataChangeOverride:&cancel];
964+
[self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
953965
}
954966

955967
#pragma mark - Handle view resizing

testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
6816DBA42318358200A51400 /* GoldenTestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA32318358200A51400 /* GoldenTestManager.m */; };
5757
68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; };
5858
68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 68D4017C2564859300ECD91A /* ContinuousTexture.m */; };
59+
F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */; };
5960
/* End PBXBuildFile section */
6061

6162
/* Begin PBXContainerItemProxy section */
@@ -172,6 +173,7 @@
172173
68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = "<group>"; };
173174
68D4017B2564859300ECD91A /* ContinuousTexture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContinuousTexture.h; sourceTree = "<group>"; };
174175
68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = "<group>"; };
176+
F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iPadGestureTests.m; sourceTree = "<group>"; };
175177
/* End PBXFileReference section */
176178

177179
/* Begin PBXFrameworksBuildPhase section */
@@ -295,6 +297,7 @@
295297
246A6610252E693A00EAB0F3 /* RenderingSelectionTest.m */,
296298
0DDEBC87258830B40065D0E8 /* SpawnEngineTest.h */,
297299
0DDEBC88258830B40065D0E8 /* SpawnEngineTest.m */,
300+
F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */,
298301
);
299302
path = ScenariosUITests;
300303
sourceTree = "<group>";
@@ -490,6 +493,7 @@
490493
6816DBA42318358200A51400 /* GoldenTestManager.m in Sources */,
491494
248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */,
492495
0D8470A4240F0B1F0030B565 /* StatusBarTest.m in Sources */,
496+
F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */,
493497
246A6611252E693A00EAB0F3 /* RenderingSelectionTest.m in Sources */,
494498
4F06F1B32473296E000AF246 /* LocalizationInitializationTest.m in Sources */,
495499
0A42BFB42447E179007E212E /* TextSemanticsFocusTest.m in Sources */,

testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ - (BOOL)application:(UIApplication*)application
5757
@"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture",
5858
@"--bogus-font-text" : @"bogus_font_text",
5959
@"--spawn-engine-works" : @"spawn_engine_works",
60+
@"--pointer-events" : @"pointer_events",
6061
};
6162
__block NSString* flutterViewControllerTestName = nil;
6263
[launchArgsMap
@@ -109,6 +110,7 @@ - (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier {
109110
FlutterEngine* engine = [self engineForTest:scenarioIdentifier];
110111
FlutterViewController* flutterViewController =
111112
[self flutterViewControllerForTest:scenarioIdentifier withEngine:engine];
113+
flutterViewController.view.accessibilityIdentifier = @"flutter_view";
112114

113115
[engine.binaryMessenger
114116
setMessageHandlerOnChannel:@"waiting_for_status"

testing/scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ - (void)testTapStatusBar {
3030
[[self.application.statusBars firstMatch] tap];
3131
}
3232

33-
XCUIElement* addTextField = self.application.textFields[@"PointerChange.add"];
33+
XCUIElement* addTextField = self.application.textFields[@"PointerChange.add:0"];
3434
BOOL exists = [addTextField waitForExistenceWithTimeout:1];
3535
XCTAssertTrue(exists, @"");
36-
XCUIElement* upTextField = self.application.textFields[@"PointerChange.up"];
36+
XCUIElement* downTextField = self.application.textFields[@"PointerChange.down:0"];
37+
exists = [downTextField waitForExistenceWithTimeout:1];
38+
XCTAssertTrue(exists, @"");
39+
XCUIElement* upTextField = self.application.textFields[@"PointerChange.up:0"];
3740
exists = [upTextField waitForExistenceWithTimeout:1];
3841
XCTAssertTrue(exists, @"");
3942
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2020 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+
#import <XCTest/XCTest.h>
6+
7+
static const NSInteger kSecondsToWaitForFlutterView = 30;
8+
9+
@interface iPadGestureTests : XCTestCase
10+
11+
@end
12+
13+
@implementation iPadGestureTests
14+
15+
- (void)setUp {
16+
[super setUp];
17+
self.continueAfterFailure = NO;
18+
}
19+
20+
#ifdef __IPHONE_15_0
21+
- (void)testPointerButtons {
22+
if (@available(iOS 15, *)) {
23+
XCTSkipUnless([XCUIDevice.sharedDevice supportsPointerInteraction],
24+
"Device does not support pointer interaction");
25+
XCUIApplication* app = [[XCUIApplication alloc] init];
26+
app.launchArguments = @[ @"--pointer-events" ];
27+
[app launch];
28+
29+
NSPredicate* predicateToFindFlutterView =
30+
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
31+
NSDictionary<NSString*, id>* _Nullable bindings) {
32+
XCUIElement* element = evaluatedObject;
33+
return [element.identifier hasPrefix:@"flutter_view"];
34+
}];
35+
XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny]
36+
elementMatchingPredicate:predicateToFindFlutterView];
37+
if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) {
38+
NSLog(@"%@", app.debugDescription);
39+
XCTFail(@"Failed due to not able to find any flutterView with %@ seconds",
40+
@(kSecondsToWaitForFlutterView));
41+
}
42+
43+
XCTAssertNotNil(flutterView);
44+
45+
[flutterView tap];
46+
// Initial add event should have buttons = 0
47+
XCTAssertTrue([app.textFields[@"PointerChange.add:0"] waitForExistenceWithTimeout:1],
48+
@"PointerChange.add event did not occur");
49+
// Normal tap should have buttons = 0, the flutter framework will ensure it has buttons = 1
50+
XCTAssertTrue([app.textFields[@"PointerChange.down:0"] waitForExistenceWithTimeout:1],
51+
@"PointerChange.down event did not occur for a normal tap");
52+
XCTAssertTrue([app.textFields[@"PointerChange.up:0"] waitForExistenceWithTimeout:1],
53+
@"PointerChange.up event did not occur for a normal tap");
54+
[flutterView rightClick];
55+
// Since each touch is its own device, we can't distinguish the other add event(s)
56+
// Right click should have buttons = 2
57+
XCTAssertTrue([app.textFields[@"PointerChange.down:2"] waitForExistenceWithTimeout:1],
58+
@"PointerChange.down event did not occur for a right-click");
59+
XCTAssertTrue([app.textFields[@"PointerChange.up:2"] waitForExistenceWithTimeout:1],
60+
@"PointerChange.up event did not occur for a right-click");
61+
NSLog(@"DebugDescriptionX: %@", app.debugDescription);
62+
}
63+
}
64+
#endif
65+
66+
@end

testing/scenario_app/lib/src/scenarios.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Map<String, ScenarioFactory> _scenarios = <String, ScenarioFactory>{
4848
'platform_view_with_continuous_texture': () => PlatformViewWithContinuousTexture(PlatformDispatcher.instance, 'Platform View', id: _viewId++),
4949
'bogus_font_text': () => BogusFontText(PlatformDispatcher.instance),
5050
'spawn_engine_works' : () => BogusFontText(PlatformDispatcher.instance),
51+
'pointer_events': () => TouchesScenario(PlatformDispatcher.instance),
5152
};
5253

5354
Map<String, dynamic> _currentScenarioParams = <String, dynamic>{};

testing/scenario_app/lib/src/touches_scenario.dart

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,24 @@ class TouchesScenario extends Scenario {
1414
/// Constructor for `TouchesScenario`.
1515
TouchesScenario(PlatformDispatcher dispatcher) : super(dispatcher);
1616

17+
@override
18+
void onBeginFrame(Duration duration) {
19+
// It is necessary to render frames for touch events to work properly on iOS
20+
final Scene scene = SceneBuilder().build();
21+
window.render(scene);
22+
scene.dispose();
23+
}
24+
1725
@override
1826
void onPointerDataPacket(PointerDataPacket packet) {
19-
sendJsonMessage(
20-
dispatcher: dispatcher,
21-
channel: 'display_data',
22-
json: <String, dynamic>{
23-
'data': packet.data[0].change.toString(),
24-
},
25-
);
27+
for (final PointerData datum in packet.data) {
28+
sendJsonMessage(
29+
dispatcher: dispatcher,
30+
channel: 'display_data',
31+
json: <String, dynamic>{
32+
'data': datum.change.toString() + ':' + datum.buttons.toString(),
33+
},
34+
);
35+
}
2636
}
2737
}

0 commit comments

Comments
 (0)