diff --git a/AUTHORS b/AUTHORS index f9021efd7e70e..83e173e620091 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,3 +18,4 @@ shoryukenn SOTEC GmbH & Co. KG Hidenori Matsubayashi Sarbagya Dhaubanjar +Callum Moffat diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 19fbb42a0d2d3..3d631d9ce0935 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -814,7 +814,8 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { // in the status bar area are available to framework code. The change type (optional) of the faked // touch is specified in the second argument. - (void)dispatchTouches:(NSSet*)touches - pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change { + pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change + event:(UIEvent*)event { if (!_engine) { return; } @@ -925,6 +926,17 @@ - (void)dispatchTouches:(NSSet*)touches pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2; } + if (@available(iOS 13.4, *)) { + if (event != nullptr) { + pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0) + ? flutter::PointerButtonMouse::kPointerButtonMousePrimary + : 0) | + (((event.buttonMask & UIEventButtonMaskSecondary) > 0) + ? flutter::PointerButtonMouse::kPointerButtonMouseSecondary + : 0); + } + } + packet->SetPointerData(pointer_index++, pointer_data); } @@ -932,24 +944,24 @@ - (void)dispatchTouches:(NSSet*)touches } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { - [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; + [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { - [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; + [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event]; } - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { - [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; + [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event]; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; + [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event]; } - (void)forceTouchesCancelled:(NSSet*)touches { flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel; - [self dispatchTouches:touches pointerDataChangeOverride:&cancel]; + [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr]; } #pragma mark - Handle view resizing diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 896e30216dbab..53ff9ba8e598d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ 6816DBA42318358200A51400 /* GoldenTestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA32318358200A51400 /* GoldenTestManager.m */; }; 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; }; 68D4017D2564859300ECD91A /* ContinuousTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 68D4017C2564859300ECD91A /* ContinuousTexture.m */; }; + F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -172,6 +173,7 @@ 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = ""; }; 68D4017B2564859300ECD91A /* ContinuousTexture.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ContinuousTexture.h; sourceTree = ""; }; 68D4017C2564859300ECD91A /* ContinuousTexture.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ContinuousTexture.m; sourceTree = ""; }; + F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = iPadGestureTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -295,6 +297,7 @@ 246A6610252E693A00EAB0F3 /* RenderingSelectionTest.m */, 0DDEBC87258830B40065D0E8 /* SpawnEngineTest.h */, 0DDEBC88258830B40065D0E8 /* SpawnEngineTest.m */, + F26F15B7268B6B5500EC54D3 /* iPadGestureTests.m */, ); path = ScenariosUITests; sourceTree = ""; @@ -490,6 +493,7 @@ 6816DBA42318358200A51400 /* GoldenTestManager.m in Sources */, 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */, 0D8470A4240F0B1F0030B565 /* StatusBarTest.m in Sources */, + F26F15B8268B6B5600EC54D3 /* iPadGestureTests.m in Sources */, 246A6611252E693A00EAB0F3 /* RenderingSelectionTest.m in Sources */, 4F06F1B32473296E000AF246 /* LocalizationInitializationTest.m in Sources */, 0A42BFB42447E179007E212E /* TextSemanticsFocusTest.m in Sources */, diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 609cb74deef00..86326d23cfae0 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -57,6 +57,7 @@ - (BOOL)application:(UIApplication*)application @"--platform-view-with-continuous-texture" : @"platform_view_with_continuous_texture", @"--bogus-font-text" : @"bogus_font_text", @"--spawn-engine-works" : @"spawn_engine_works", + @"--pointer-events" : @"pointer_events", }; __block NSString* flutterViewControllerTestName = nil; [launchArgsMap @@ -109,6 +110,7 @@ - (void)setupFlutterViewControllerTest:(NSString*)scenarioIdentifier { FlutterEngine* engine = [self engineForTest:scenarioIdentifier]; FlutterViewController* flutterViewController = [self flutterViewControllerForTest:scenarioIdentifier withEngine:engine]; + flutterViewController.view.accessibilityIdentifier = @"flutter_view"; [engine.binaryMessenger setMessageHandlerOnChannel:@"waiting_for_status" diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m index 5192b62e461fc..6f9ce64058653 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m @@ -30,10 +30,13 @@ - (void)testTapStatusBar { [[self.application.statusBars firstMatch] tap]; } - XCUIElement* addTextField = self.application.textFields[@"PointerChange.add"]; + XCUIElement* addTextField = self.application.textFields[@"PointerChange.add:0"]; BOOL exists = [addTextField waitForExistenceWithTimeout:1]; XCTAssertTrue(exists, @""); - XCUIElement* upTextField = self.application.textFields[@"PointerChange.up"]; + XCUIElement* downTextField = self.application.textFields[@"PointerChange.down:0"]; + exists = [downTextField waitForExistenceWithTimeout:1]; + XCTAssertTrue(exists, @""); + XCUIElement* upTextField = self.application.textFields[@"PointerChange.up:0"]; exists = [upTextField waitForExistenceWithTimeout:1]; XCTAssertTrue(exists, @""); } diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m new file mode 100644 index 0000000000000..2f6d9c9237f12 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/iPadGestureTests.m @@ -0,0 +1,66 @@ +// Copyright 2020 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +static const NSInteger kSecondsToWaitForFlutterView = 30; + +@interface iPadGestureTests : XCTestCase + +@end + +@implementation iPadGestureTests + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +#ifdef __IPHONE_15_0 +- (void)testPointerButtons { + if (@available(iOS 15, *)) { + XCTSkipUnless([XCUIDevice.sharedDevice supportsPointerInteraction], + "Device does not support pointer interaction"); + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--pointer-events" ]; + [app launch]; + + NSPredicate* predicateToFindFlutterView = + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, + NSDictionary* _Nullable bindings) { + XCUIElement* element = evaluatedObject; + return [element.identifier hasPrefix:@"flutter_view"]; + }]; + XCUIElement* flutterView = [[app descendantsMatchingType:XCUIElementTypeAny] + elementMatchingPredicate:predicateToFindFlutterView]; + if (![flutterView waitForExistenceWithTimeout:kSecondsToWaitForFlutterView]) { + NSLog(@"%@", app.debugDescription); + XCTFail(@"Failed due to not able to find any flutterView with %@ seconds", + @(kSecondsToWaitForFlutterView)); + } + + XCTAssertNotNil(flutterView); + + [flutterView tap]; + // Initial add event should have buttons = 0 + XCTAssertTrue([app.textFields[@"PointerChange.add:0"] waitForExistenceWithTimeout:1], + @"PointerChange.add event did not occur"); + // Normal tap should have buttons = 0, the flutter framework will ensure it has buttons = 1 + XCTAssertTrue([app.textFields[@"PointerChange.down:0"] waitForExistenceWithTimeout:1], + @"PointerChange.down event did not occur for a normal tap"); + XCTAssertTrue([app.textFields[@"PointerChange.up:0"] waitForExistenceWithTimeout:1], + @"PointerChange.up event did not occur for a normal tap"); + [flutterView rightClick]; + // Since each touch is its own device, we can't distinguish the other add event(s) + // Right click should have buttons = 2 + XCTAssertTrue([app.textFields[@"PointerChange.down:2"] waitForExistenceWithTimeout:1], + @"PointerChange.down event did not occur for a right-click"); + XCTAssertTrue([app.textFields[@"PointerChange.up:2"] waitForExistenceWithTimeout:1], + @"PointerChange.up event did not occur for a right-click"); + NSLog(@"DebugDescriptionX: %@", app.debugDescription); + } +} +#endif + +@end diff --git a/testing/scenario_app/lib/src/scenarios.dart b/testing/scenario_app/lib/src/scenarios.dart index 4d68c1a0ccfb6..c8527f4e0f5c9 100644 --- a/testing/scenario_app/lib/src/scenarios.dart +++ b/testing/scenario_app/lib/src/scenarios.dart @@ -48,6 +48,7 @@ Map _scenarios = { 'platform_view_with_continuous_texture': () => PlatformViewWithContinuousTexture(PlatformDispatcher.instance, 'Platform View', id: _viewId++), 'bogus_font_text': () => BogusFontText(PlatformDispatcher.instance), 'spawn_engine_works' : () => BogusFontText(PlatformDispatcher.instance), + 'pointer_events': () => TouchesScenario(PlatformDispatcher.instance), }; Map _currentScenarioParams = {}; diff --git a/testing/scenario_app/lib/src/touches_scenario.dart b/testing/scenario_app/lib/src/touches_scenario.dart index 6d193f0ae44c3..24da2c1ca5c0d 100644 --- a/testing/scenario_app/lib/src/touches_scenario.dart +++ b/testing/scenario_app/lib/src/touches_scenario.dart @@ -14,14 +14,24 @@ class TouchesScenario extends Scenario { /// Constructor for `TouchesScenario`. TouchesScenario(PlatformDispatcher dispatcher) : super(dispatcher); + @override + void onBeginFrame(Duration duration) { + // It is necessary to render frames for touch events to work properly on iOS + final Scene scene = SceneBuilder().build(); + window.render(scene); + scene.dispose(); + } + @override void onPointerDataPacket(PointerDataPacket packet) { - sendJsonMessage( - dispatcher: dispatcher, - channel: 'display_data', - json: { - 'data': packet.data[0].change.toString(), - }, - ); + for (final PointerData datum in packet.data) { + sendJsonMessage( + dispatcher: dispatcher, + channel: 'display_data', + json: { + 'data': datum.change.toString() + ':' + datum.buttons.toString(), + }, + ); + } } }