diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h index 49032f87d3a91..891ae28928a1e 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h @@ -7,6 +7,8 @@ #import +#include + #import "FlutterBinaryMessenger.h" #import "FlutterDartProject.h" #import "FlutterMacros.h" @@ -15,6 +17,17 @@ // TODO: Merge this file with the iOS FlutterEngine.h. +/** + * The view ID for APIs that don't support multi-view. + * + * Some single-view APIs will eventually be replaced by their multi-view + * variant. During the deprecation period, the single-view APIs will coexist with + * and work with the multi-view APIs as if the other views don't exist. For + * backward compatibility, single-view APIs will always operate the view with + * this ID. Also, the first view assigned to the engine will also have this ID. + */ +extern const uint64_t kFlutterDefaultViewId; + @class FlutterViewController; /** @@ -67,6 +80,15 @@ FLUTTER_DARWIN_EXPORT * * The default view always has ID kFlutterDefaultViewId, and is the view * operated by the APIs that do not have a view ID specified. + * + * Setting this field from nil to a non-nil view controller also updates + * the view controller's engine and ID. + * + * Setting this field from non-nil to nil will terminate the engine if + * allowHeadlessExecution is NO. + * + * Setting this field from non-nil to a different non-nil FlutterViewController + * is prohibited and will throw an assertion error. */ @property(nonatomic, nullable, weak) FlutterViewController* viewController; diff --git a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h index 4dea666e1262c..e4d8df7d4c48d 100644 --- a/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h +++ b/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h @@ -25,6 +25,25 @@ typedef NS_ENUM(NSInteger, FlutterMouseTrackingMode) { /** * Controls a view that displays Flutter content and manages input. + * + * A FlutterViewController works with a FlutterEngine. Upon creation, the view + * controller is always added to an engine, either a given engine, or it implicitly + * creates an engine and add itself to that engine. + * + * The FlutterEngine assigns each view controller attached to it a unique ID. + * Each view controller corresponds to a view, and the ID is used by the framework + * to specify which view to operate. + * + * A FlutterViewController can also be unattached to an engine after it is manually + * unset from the engine, or transiently during the initialization process. + * An unattached view controller is invalid. Whether the view controller is attached + * can be queried using FlutterViewController#attached. + * + * The FlutterViewController strongly references the FlutterEngine, while + * the engine weakly the view controller. When a FlutterViewController is deallocated, + * it automatically removes itself from its attached engine. When a FlutterEngine + * has no FlutterViewControllers attached, it might shut down itself or not depending + * on its configuration. */ FLUTTER_DARWIN_EXPORT @interface FlutterViewController : NSViewController @@ -34,6 +53,16 @@ FLUTTER_DARWIN_EXPORT */ @property(nonatomic, nonnull, readonly) FlutterEngine* engine; +/** + * The identifier for this view controller. + * + * The ID is assigned by FlutterEngine when the view controller is attached. + * + * If the view controller is unattached (see FlutterViewController#attached), + * reading this property throws an assertion. + */ +@property(nonatomic, readonly) uint64_t id; + /** * The style of mouse tracking to use for the view. Defaults to * FlutterMouseTrackingModeInKeyWindow. @@ -43,6 +72,12 @@ FLUTTER_DARWIN_EXPORT /** * Initializes a controller that will run the given project. * + * In this initializer, this controller creates an engine, and is attached to + * that engine as the default controller. In this way, this controller can not + * be set to other engines. This initializer is suitable for the first Flutter + * view controller of the app. To use the controller with an existing engine, + * use initWithEngine:nibName:bundle: instead. + * * @param project The project to run in this view controller. If nil, a default `FlutterDartProject` * will be used. */ @@ -56,7 +91,8 @@ FLUTTER_DARWIN_EXPORT /** * Initializes this FlutterViewController with the specified `FlutterEngine`. * - * The initialized viewcontroller will attach itself to the engine as part of this process. + * This initializer is suitable for both the first Flutter view controller and + * the following ones of the app. * * @param engine The `FlutterEngine` instance to attach to. Cannot be nil. * @param nibName The NIB name to initialize this controller with. @@ -65,6 +101,12 @@ FLUTTER_DARWIN_EXPORT - (nonnull instancetype)initWithEngine:(nonnull FlutterEngine*)engine nibName:(nullable NSString*)nibName bundle:(nullable NSBundle*)nibBundle NS_DESIGNATED_INITIALIZER; + +/** + * Return YES if the view controller is attached to an engine. + */ +- (BOOL)attached; + /** * Invoked by the engine right before the engine is restarted. * diff --git a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm index 0ad11af0e3e8f..d493da6461719 100644 --- a/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMacTest.mm @@ -51,26 +51,22 @@ @implementation AccessibilityBridgeTestEngine namespace { // Returns an engine configured for the text fixture resource configuration. -FlutterEngine* CreateTestEngine() { +FlutterViewController* CreateTestViewController() { NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - return [[AccessibilityBridgeTestEngine alloc] initWithName:@"test" - project:project - allowHeadlessExecution:true]; + FlutterEngine* engine = [[AccessibilityBridgeTestEngine alloc] initWithName:@"test" + project:project + allowHeadlessExecution:true]; + return [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; } } // namespace TEST(AccessibilityBridgeMacTest, sendsAccessibilityCreateNotificationToWindowOfFlutterView) { - FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; [viewController loadView]; - [engine setViewController:viewController]; NSWindow* expectedTarget = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask @@ -122,14 +118,9 @@ @implementation AccessibilityBridgeTestEngine } TEST(AccessibilityBridgeMacTest, doesNotSendAccessibilityCreateNotificationWhenHeadless) { - FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; [viewController loadView]; - [engine setViewController:viewController]; // Setting up bridge so that the AccessibilityBridgeMacDelegateSpy // can query semantics information from. engine.semanticsEnabled = YES; @@ -173,15 +164,9 @@ @implementation AccessibilityBridgeTestEngine } TEST(AccessibilityBridgeMacTest, doesNotSendAccessibilityCreateNotificationWhenNoWindow) { - FlutterEngine* engine = CreateTestEngine(); - // Create a view controller without attaching it to a window. - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; [viewController loadView]; - [engine setViewController:viewController]; // Setting up bridge so that the AccessibilityBridgeMacDelegateSpy // can query semantics information from. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h index 99983e8ddb9bf..ba20f4dadbbfa 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterCompositor.h @@ -10,7 +10,6 @@ #include "flutter/fml/macros.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterPlatformViewController.h" -#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h" #include "flutter/shell/platform/embedder/embedder.h" diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 870ca4802f3d9..07a9836f414c7 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -19,6 +19,8 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewEngineProvider.h" #include "flutter/shell/platform/embedder/embedder.h" +const uint64_t kFlutterDefaultViewId = 0; + /** * Constructs and returns a FlutterLocale struct corresponding to |locale|, which must outlive * the returned struct. @@ -80,6 +82,8 @@ @interface FlutterEngine () */ @property(nonatomic, strong) NSMutableArray* isResponseValid; +- (nullable FlutterViewController*)viewControllerForId:(uint64_t)viewId; + /** * Sends the list of user-preferred locales to the Flutter engine. */ @@ -213,8 +217,6 @@ @implementation FlutterEngine { // when the engine is destroyed. std::unique_ptr _macOSCompositor; - FlutterViewEngineProvider* _viewProvider; - // FlutterCompositor is copied and used in embedder.cc. FlutterCompositor _compositor; @@ -248,7 +250,6 @@ - (instancetype)initWithName:(NSString*)labelPrefix _currentMessengerConnection = 1; _allowHeadlessExecution = allowHeadlessExecution; _semanticsEnabled = NO; - _viewProvider = [[FlutterViewEngineProvider alloc] initWithEngine:self]; _isResponseValid = [[NSMutableArray alloc] initWithCapacity:1]; [_isResponseValid addObject:@YES]; @@ -411,29 +412,41 @@ - (void)loadAOTData:(NSString*)assetsDir { } - (void)setViewController:(FlutterViewController*)controller { - if (_viewController != controller) { + if (_viewController == controller) { + // From nil to nil, or from non-nil to the same controller. + return; + } + if (_viewController == nil && controller != nil) { + // From nil to non-nil. + NSAssert(controller.engine == nil, + @"Failed to set view controller to the engine: " + @"The given FlutterViewController is already attached to an engine %@. " + @"If you wanted to create an FlutterViewController and set it to an existing engine, " + @"you should create it with init(engine:, nibName, bundle:) instead.", + controller.engine); _viewController = controller; - - if (_semanticsEnabled && _bridge) { - _bridge->UpdateDefaultViewController(_viewController); - } - - if (!controller && !_allowHeadlessExecution) { + [_viewController attachToEngine:self withId:kFlutterDefaultViewId]; + } else if (_viewController != nil && controller == nil) { + // From non-nil to nil. + [_viewController detachFromEngine]; + _viewController = nil; + if (!_allowHeadlessExecution) { [self shutDownEngine]; } + } else { + // From non-nil to a different non-nil view controller. + NSAssert(NO, + @"Failed to set view controller to the engine: " + @"The engine already has a default view controller %@. " + @"If you wanted to make the default view render in a different window, " + @"you should attach the current view controller to the window instead.", + _viewController); } } - (FlutterCompositor*)createFlutterCompositor { - // TODO(richardjcai): Add support for creating a FlutterCompositor - // with a nil _viewController for headless engines. - // https://github.com/flutter/flutter/issues/71606 - if (!_viewController) { - return nil; - } - - _macOSCompositor = - std::make_unique(_viewProvider, _platformViewController); + _macOSCompositor = std::make_unique( + [[FlutterViewEngineProvider alloc] initWithEngine:self], _platformViewController); _compositor = {}; _compositor.struct_size = sizeof(FlutterCompositor); @@ -539,10 +552,10 @@ - (nonnull NSString*)executableName { } - (void)updateWindowMetrics { - if (!_engine || !_viewController.viewLoaded) { + if (!_engine || !self.viewController.viewLoaded) { return; } - NSView* view = _viewController.flutterView; + NSView* view = self.viewController.flutterView; CGRect scaledBounds = [view convertRectToBacking:view.bounds]; CGSize scaledSize = scaledBounds.size; double pixelRatio = view.bounds.size.width == 0 ? 1 : scaledSize.width / view.bounds.size.width; @@ -603,6 +616,17 @@ - (FlutterPlatformViewController*)platformViewController { #pragma mark - Private methods +- (FlutterViewController*)viewControllerForId:(uint64_t)viewId { + // TODO(dkwingsmt): The engine only supports single-view, therefore it + // only processes the default ID. After the engine supports multiple views, + // this method should be able to return the view for any IDs. + NSAssert(viewId == kFlutterDefaultViewId, @"Unexpected view ID %llu", viewId); + if (viewId == kFlutterDefaultViewId) { + return _viewController; + } + return nil; +} + - (void)sendUserLocales { if (!self.running) { return; @@ -679,9 +703,7 @@ - (void)shutDownEngine { return; } - if (_viewController && _viewController.flutterView) { - [_viewController.flutterView shutdown]; - } + [self.viewController.flutterView shutdown]; FlutterEngineResult result = _embedderAPI.Deinitialize(_engine); if (result != kSuccess) { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index a162fbb64c3cf..bf8da30fe1df8 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -134,14 +134,11 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]); EXPECT_TRUE(engine.running); - NSString* fixtures = @(flutter::testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; viewController.flutterView.frame = CGRectMake(0, 0, 800, 600); - [engine setViewController:viewController]; latch.Wait(); } @@ -166,14 +163,11 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_TRUE([engine runWithEntrypoint:@"backgroundTest"]); EXPECT_TRUE(engine.running); - NSString* fixtures = @(flutter::testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; viewController.flutterView.frame = CGRectMake(0, 0, 800, 600); - [engine setViewController:viewController]; viewController.flutterView.backgroundColor = [NSColor whiteColor]; latch.Wait(); @@ -193,13 +187,10 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable })); EXPECT_TRUE([engine runWithEntrypoint:@"main"]); // Set up view controller. - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; - [engine setViewController:viewController]; // Enable the semantics. bool enabled_called = false; engine.embedderAPI.UpdateSemanticsEnabled = @@ -337,6 +328,8 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable FlutterSemanticsNode nodes[] = {root, child1}; update.nodes = nodes; update.custom_actions_count = 0; + // This call updates semantics for the default view, which does not exist, + // and therefore this call is invalid. But the engine should not crash. update_semantics_callback(&update, (__bridge void*)engine); // No crashes. @@ -355,93 +348,6 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_EQ(engine.viewController, nil); } -TEST_F(FlutterEngineTest, ResetsAccessibilityBridgeWhenSetsNewViewController) { - FlutterEngine* engine = GetFlutterEngine(); - // Capture the update callbacks before the embedder API initializes. - auto original_init = engine.embedderAPI.Initialize; - std::function update_semantics_callback; - engine.embedderAPI.Initialize = MOCK_ENGINE_PROC( - Initialize, ([&update_semantics_callback, &original_init]( - size_t version, const FlutterRendererConfig* config, - const FlutterProjectArgs* args, void* user_data, auto engine_out) { - update_semantics_callback = args->update_semantics_callback; - return original_init(version, config, args, user_data, engine_out); - })); - EXPECT_TRUE([engine runWithEntrypoint:@"main"]); - // Set up view controller. - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; - [viewController loadView]; - [engine setViewController:viewController]; - // Enable the semantics. - bool enabled_called = false; - engine.embedderAPI.UpdateSemanticsEnabled = - MOCK_ENGINE_PROC(UpdateSemanticsEnabled, ([&enabled_called](auto engine, bool enabled) { - enabled_called = enabled; - return kSuccess; - })); - engine.semanticsEnabled = YES; - EXPECT_TRUE(enabled_called); - // Send flutter semantics updates. - FlutterSemanticsNode root; - root.id = 0; - root.flags = static_cast(0); - root.actions = static_cast(0); - root.text_selection_base = -1; - root.text_selection_extent = -1; - root.label = "root"; - root.hint = ""; - root.value = ""; - root.increased_value = ""; - root.decreased_value = ""; - root.tooltip = ""; - root.child_count = 1; - int32_t children[] = {1}; - root.children_in_traversal_order = children; - root.custom_accessibility_actions_count = 0; - - FlutterSemanticsNode child1; - child1.id = 1; - child1.flags = static_cast(0); - child1.actions = static_cast(0); - child1.text_selection_base = -1; - child1.text_selection_extent = -1; - child1.label = "child 1"; - child1.hint = ""; - child1.value = ""; - child1.increased_value = ""; - child1.decreased_value = ""; - child1.tooltip = ""; - child1.child_count = 0; - child1.custom_accessibility_actions_count = 0; - - FlutterSemanticsUpdate update; - update.nodes_count = 2; - FlutterSemanticsNode nodes[] = {root, child1}; - update.nodes = nodes; - update.custom_actions_count = 0; - update_semantics_callback(&update, (__bridge void*)engine); - - auto native_root = engine.accessibilityBridge.lock()->GetFlutterPlatformNodeDelegateFromID(0); - EXPECT_FALSE(native_root.expired()); - - // Set up a new view controller. - FlutterViewController* newViewController = - [[FlutterViewController alloc] initWithProject:project]; - [newViewController loadView]; - [engine setViewController:newViewController]; - - auto new_native_root = engine.accessibilityBridge.lock()->GetFlutterPlatformNodeDelegateFromID(0); - // The tree is recreated and the old tree will be destroyed. - EXPECT_FALSE(new_native_root.expired()); - EXPECT_TRUE(native_root.expired()); - - [engine setViewController:nil]; -} - TEST_F(FlutterEngineTest, NativeCallbacks) { fml::AutoResetWaitableEvent latch; bool latch_called = false; @@ -465,10 +371,11 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"test" project:project]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; viewController.flutterView.frame = CGRectMake(0, 0, 800, 600); - [engine setViewController:viewController]; EXPECT_TRUE([engine runWithEntrypoint:@"canCompositePlatformViews"]); diff --git a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm index a5809bef78d92..84372964f9501 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterPlatformNodeDelegateMacTest.mm @@ -19,18 +19,19 @@ namespace flutter::testing { namespace { -// Returns an engine configured for the text fixture resource configuration. -FlutterEngine* CreateTestEngine() { +// Returns a view controller configured for the text fixture resource configuration. +FlutterViewController* CreateTestViewController() { NSString* fixtures = @(testing::GetFixturesPath()); FlutterDartProject* project = [[FlutterDartProject alloc] initWithAssetsPath:fixtures ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - return [[FlutterEngine alloc] initWithName:@"test" project:project allowHeadlessExecution:true]; + return [[FlutterViewController alloc] initWithProject:project]; } } // namespace TEST(FlutterPlatformNodeDelegateMac, Basics) { - FlutterEngine* engine = CreateTestEngine(); + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); // Initialize ax node data. @@ -65,7 +66,8 @@ } TEST(FlutterPlatformNodeDelegateMac, SelectableTextHasCorrectSemantics) { - FlutterEngine* engine = CreateTestEngine(); + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); // Initialize ax node data. @@ -106,7 +108,8 @@ } TEST(FlutterPlatformNodeDelegateMac, SelectableTextWithoutSelectionReturnZeroRange) { - FlutterEngine* engine = CreateTestEngine(); + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; engine.semanticsEnabled = YES; auto bridge = engine.accessibilityBridge.lock(); // Initialize ax node data. @@ -144,15 +147,8 @@ // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape) TEST(FlutterPlatformNodeDelegateMac, CanPerformAction) { - FlutterEngine* engine = CreateTestEngine(); - - // Set up view controller. - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; - [engine setViewController:viewController]; + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; // Attach the view to a NSWindow. NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) @@ -222,14 +218,9 @@ // NOLINTEND(clang-analyzer-core.StackAddressEscape) TEST(FlutterPlatformNodeDelegateMac, TextFieldUsesFlutterTextField) { - FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = CreateTestViewController(); + FlutterEngine* engine = viewController.engine; [viewController loadView]; - [engine setViewController:viewController]; // Unit test localization is unnecessary. // NOLINTNEXTLINE(clang-analyzer-optin.osx.cocoa.localizability.NonLocalizedStringChecker) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm index 9f4a39d4b3818..94c095d354d25 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputPluginTest.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h" @@ -37,6 +38,16 @@ @interface NSTextInputContext (Private) - (BOOL)isActive; @end +@interface TextInputTestViewController : FlutterViewController +@end + +@implementation TextInputTestViewController +- (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device + commandQueue:(id)commandQueue { + return OCMClassMock([NSView class]); +} +@end + @interface FlutterInputPluginTestObjc : NSObject - (bool)testEmptyCompositionRange; - (bool)testClearClientDuringComposing; @@ -45,7 +56,7 @@ - (bool)testClearClientDuringComposing; @implementation FlutterInputPluginTestObjc - (bool)testEmptyCompositionRange { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -110,7 +121,7 @@ - (bool)testEmptyCompositionRange { } - (bool)testSetMarkedTextWithSelectionChange { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -178,7 +189,7 @@ - (bool)testSetMarkedTextWithSelectionChange { } - (bool)testSetMarkedTextWithReplacementRange { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -246,7 +257,7 @@ - (bool)testSetMarkedTextWithReplacementRange { } - (bool)testComposingRegionRemovedByFramework { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -324,7 +335,7 @@ - (bool)testComposingRegionRemovedByFramework { - (bool)testClearClientDuringComposing { // Set up FlutterTextInputPlugin. - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -377,26 +388,18 @@ - (bool)testClearClientDuringComposing { } - (bool)testFirstRectForCharacterRange { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - FlutterViewController* controllerMock = OCMClassMock([FlutterViewController class]); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock engine]) - .andReturn(engineMock); - - id viewMock = OCMClassMock([NSView class]); + FlutterViewController* controllerMock = + [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil]; + [controllerMock loadView]; + id viewMock = controllerMock.flutterView; OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [viewMock bounds]) .andReturn(NSMakeRect(0, 0, 200, 200)); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - controllerMock.viewLoaded) - .andReturn(YES); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock flutterView]) - .andReturn(viewMock); id windowMock = OCMClassMock([NSWindow class]); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) @@ -453,26 +456,18 @@ - (bool)testFirstRectForCharacterRange { } - (bool)testFirstRectForCharacterRangeAtInfinity { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - FlutterViewController* controllerMock = OCMClassMock([FlutterViewController class]); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock engine]) - .andReturn(engineMock); - - id viewMock = OCMClassMock([NSView class]); + FlutterViewController* controllerMock = + [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil]; + [controllerMock loadView]; + id viewMock = controllerMock.flutterView; OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [viewMock bounds]) .andReturn(NSMakeRect(0, 0, 200, 200)); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - controllerMock.viewLoaded) - .andReturn(YES); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock flutterView]) - .andReturn(viewMock); id windowMock = OCMClassMock([NSWindow class]); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) @@ -515,26 +510,18 @@ - (bool)testFirstRectForCharacterRangeAtInfinity { } - (bool)testFirstRectForCharacterRangeWithEsotericAffineTransform { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) .andReturn(binaryMessengerMock); - FlutterViewController* controllerMock = OCMClassMock([FlutterViewController class]); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock engine]) - .andReturn(engineMock); - - id viewMock = OCMClassMock([NSView class]); + FlutterViewController* controllerMock = + [[TextInputTestViewController alloc] initWithEngine:engineMock nibName:nil bundle:nil]; + [controllerMock loadView]; + id viewMock = controllerMock.flutterView; OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [viewMock bounds]) .andReturn(NSMakeRect(0, 0, 200, 200)); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - controllerMock.viewLoaded) - .andReturn(YES); - OCMStub( // NOLINT(google-objc-avoid-throwing-exception) - [controllerMock flutterView]) - .andReturn(viewMock); id windowMock = OCMClassMock([NSWindow class]); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) @@ -595,7 +582,7 @@ - (bool)testFirstRectForCharacterRangeWithEsotericAffineTransform { } - (bool)testSetEditingStateWithTextEditingDelta { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -651,7 +638,7 @@ - (bool)testSetEditingStateWithTextEditingDelta { } - (bool)testOperationsThatTriggerDelta { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -768,7 +755,7 @@ - (bool)testOperationsThatTriggerDelta { } - (bool)testComposingWithDelta { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -1005,7 +992,7 @@ - (bool)testComposingWithDelta { } - (bool)testComposingWithDeltasWhenSelectionIsActive { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -1134,7 +1121,7 @@ - (bool)testPerformKeyEquivalent { } - (bool)unhandledKeyEquivalent { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -1210,7 +1197,7 @@ - (bool)unhandledKeyEquivalent { } - (bool)testLocalTextAndSelectionUpdateAfterDelta { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -1271,7 +1258,7 @@ - (bool)testLocalTextAndSelectionUpdateAfterDelta { } - (bool)testSelectorsAreForwardedToFramework { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -1414,13 +1401,10 @@ - (bool)testSelectorsAreForwardedToFramework { TEST(FlutterTextInputPluginTest, CanWorkWithFlutterTextField) { FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; - [engine setViewController:viewController]; // Create a NSWindow so that the native text field can become first responder. NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask @@ -1483,13 +1467,10 @@ - (bool)testSelectorsAreForwardedToFramework { TEST(FlutterTextInputPluginTest, CanNotBecomeResponderIfNoViewController) { FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; - [engine setViewController:viewController]; // Creates a NSWindow so that the native text field can become first responder. NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask @@ -1520,13 +1501,10 @@ - (bool)testSelectorsAreForwardedToFramework { TEST(FlutterTextInputPluginTest, IsAddedAndRemovedFromViewHierarchy) { FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; - [engine setViewController:viewController]; NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask diff --git a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm index 287b12713c24a..4e4b821a6115d 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObjectTest.mm @@ -28,13 +28,10 @@ TEST(FlutterTextInputSemanticsObjectTest, DoesInitialize) { FlutterEngine* engine = CreateTestEngine(); { - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; - [engine setViewController:viewController]; // Create a NSWindow so that the native text field can become first responder. NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 800, 600) styleMask:NSBorderlessWindowMask diff --git a/shell/platform/darwin/macos/framework/Source/FlutterView.h b/shell/platform/darwin/macos/framework/Source/FlutterView.h index ba70833b46b76..73425737fbd3a 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterView.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterView.h @@ -9,17 +9,6 @@ #include -/** - * The view ID for APIs that don't support multi-view. - * - * Some single-view APIs will eventually be replaced by their multi-view - * variant. During the deprecation period, the single-view APIs will coexist with - * and work with the multi-view APIs as if the other views don't exist. For - * backward compatibility, single-view APIs will always operate the view with - * this ID. Also, the first view assigned to the engine will also have this ID. - */ -constexpr uint64_t kFlutterDefaultViewId = 0; - /** * Listener for view resizing. */ diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm index 2c9e76e65990d..4d41d066c8fd4 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController.mm @@ -286,21 +286,34 @@ - (NSArray*)accessibilityChildren { @implementation FlutterViewController { // The project to run in this controller's engine. FlutterDartProject* _project; + + uint64_t _id; } +@dynamic id; @dynamic view; /** * Performs initialization that's common between the different init paths. */ -static void CommonInit(FlutterViewController* controller) { - if (!controller->_engine) { - controller->_engine = [[FlutterEngine alloc] initWithName:@"io.flutter" - project:controller->_project - allowHeadlessExecution:NO]; +static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) { + if (!engine) { + engine = [[FlutterEngine alloc] initWithName:@"io.flutter" + project:controller->_project + allowHeadlessExecution:NO]; } + NSCAssert(controller.engine == nil, + @"The FlutterViewController is unexpectedly attached to " + @"engine %@ before initialization.", + controller.engine); + engine.viewController = controller; + NSCAssert(controller.engine != nil, + @"The FlutterViewController unexpectedly stays unattached after initialization. " + @"In unit tests, this is likely because either the FlutterViewController or " + @"the FlutterEngine is mocked. Please subclass these classes instead."); controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow; controller->_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:controller]; + [controller initializeKeyboard]; // macOS fires this message when changing IMEs. CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter(); __weak FlutterViewController* weakSelf = controller; @@ -313,7 +326,7 @@ - (instancetype)initWithCoder:(NSCoder*)coder { self = [super initWithCoder:coder]; NSAssert(self, @"Super init cannot be nil"); - CommonInit(self); + CommonInit(self, nil); return self; } @@ -321,7 +334,7 @@ - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBun self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; NSAssert(self, @"Super init cannot be nil"); - CommonInit(self); + CommonInit(self, nil); return self; } @@ -330,7 +343,7 @@ - (instancetype)initWithProject:(nullable FlutterDartProject*)project { NSAssert(self, @"Super init cannot be nil"); _project = project; - CommonInit(self); + CommonInit(self, nil); return self; } @@ -346,13 +359,7 @@ - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { - _engine = engine; - CommonInit(self); - if (engine.running) { - [self loadView]; - engine.viewController = self; - [self initializeKeyboard]; - } + CommonInit(self, engine); } return self; @@ -370,9 +377,7 @@ - (void)loadView { NSLog(@"Unable to create FlutterView; no MTLDevice or MTLCommandQueue available."); return; } - flutterView = [[FlutterView alloc] initWithMTLDevice:device - commandQueue:commandQueue - reshapeListener:self]; + flutterView = [self createFlutterViewWithMTLDevice:device commandQueue:commandQueue]; if (_backgroundColor != nil) { [flutterView setBackgroundColor:_backgroundColor]; } @@ -423,16 +428,33 @@ - (void)setBackgroundColor:(NSColor*)color { [_flutterView setBackgroundColor:_backgroundColor]; } +- (uint64_t)id { + NSAssert([self attached], @"This view controller is not attched."); + return _id; +} + - (void)onPreEngineRestart { [self initializeKeyboard]; } +- (void)attachToEngine:(nonnull FlutterEngine*)engine withId:(uint64_t)viewId { + NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine); + _engine = engine; + _id = viewId; +} + +- (void)detachFromEngine { + NSAssert(_engine != nil, @"Not attached to any engine."); + _engine = nil; +} + +- (BOOL)attached { + return _engine != nil; +} + #pragma mark - Private methods - (BOOL)launchEngine { - [self initializeKeyboard]; - - _engine.viewController = self; if (![_engine runWithEntrypoint:nil]) { return NO; } @@ -691,6 +713,13 @@ - (void)onAccessibilityStatusChanged:(BOOL)enabled { } } +- (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id)device + commandQueue:(id)commandQueue { + return [[FlutterView alloc] initWithMTLDevice:device + commandQueue:commandQueue + reshapeListener:self]; +} + - (void)onKeyboardLayoutChanged { _keyboardLayoutData = nil; if (_keyboardLayoutNotifier != nil) { diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm index dbc98870a0661..ea96d6914d872 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTest.mm @@ -11,6 +11,7 @@ #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterDartProject_Internal.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngineTestUtils.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewControllerTestUtils.h" @@ -136,11 +137,9 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification, TEST(FlutterViewController, ReparentsPluginWhenAccessibilityDisabled) { FlutterEngine* engine = CreateTestEngine(); - NSString* fixtures = @(testing::GetFixturesPath()); - FlutterDartProject* project = [[FlutterDartProject alloc] - initWithAssetsPath:fixtures - ICUDataPath:[fixtures stringByAppendingString:@"/icudtl.dat"]]; - FlutterViewController* viewController = [[FlutterViewController alloc] initWithProject:project]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; [viewController loadView]; [engine setViewController:viewController]; // Creates a NSWindow so that sub view can be first responder. @@ -214,7 +213,7 @@ id MockGestureEvent(NSEventType type, NSEventPhase phase, double magnification, @implementation FlutterViewControllerTestObjC - (bool)testKeyEventsAreSentToFramework { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -252,7 +251,7 @@ - (bool)testKeyEventsAreSentToFramework { } - (bool)testKeyEventsArePropagatedIfNotHandled { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -307,7 +306,7 @@ - (bool)testKeyEventsArePropagatedIfNotHandled { } - (bool)testFlutterViewIsConfigured { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock]; OCMStub([engineMock renderer]).andReturn(renderer_); @@ -328,7 +327,7 @@ - (bool)testFlutterViewIsConfigured { } - (bool)testFlagsChangedEventsArePropagatedIfNotHandled { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -381,7 +380,7 @@ - (bool)testFlagsChangedEventsArePropagatedIfNotHandled { } - (bool)testKeyEventsAreNotPropagatedIfHandled { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -436,7 +435,7 @@ - (bool)testKeyEventsAreNotPropagatedIfHandled { } - (bool)testKeyboardIsRestartedOnEngineRestart { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); id binaryMessengerMock = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); OCMStub( // NOLINT(google-objc-avoid-throwing-exception) [engineMock binaryMessenger]) @@ -498,7 +497,7 @@ + (void)respondFalseForSendEvent:(const FlutterKeyEvent&)event } - (bool)testTrackpadGesturesAreSentToFramework { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); // Need to return a real renderer to allow view controller to load. FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock]; OCMStub([engineMock renderer]).andReturn(renderer_); @@ -794,7 +793,7 @@ - (bool)testTrackpadGesturesAreSentToFramework { } - (bool)testViewWillAppearCalledMultipleTimes { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engineMock nibName:@"" bundle:nil]; @@ -804,7 +803,7 @@ - (bool)testViewWillAppearCalledMultipleTimes { } - (bool)testModifierKeysAreSynthesizedOnMouseMove { - id engineMock = OCMClassMock([FlutterEngine class]); + id engineMock = flutter::testing::CreateMockFlutterEngine(@""); // Need to return a real renderer to allow view controller to load. FlutterRenderer* renderer_ = [[FlutterRenderer alloc] initWithFlutterEngine:engineMock]; OCMStub([engineMock renderer]).andReturn(renderer_); @@ -825,7 +824,6 @@ - (bool)testModifierKeysAreSynthesizedOnMouseMove { nibName:@"" bundle:nil]; [viewController loadView]; - [engineMock setViewController:viewController]; [viewController viewWillAppear]; // Zeroed modifier flag should not synthesize events. diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h index 19870cf5b183b..c60c5354b8d8e 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/macos/framework/Source/AccessibilityBridgeMac.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardViewDelegate.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" @@ -23,9 +24,27 @@ */ - (BOOL)isDispatchingKeyEvent:(nonnull NSEvent*)event; +/** + * Set the `engine` and `id` of this controller. + * + * This method is called by FlutterEngine. + */ +- (void)attachToEngine:(nonnull FlutterEngine*)engine withId:(uint64_t)viewId; + +/** + * Reset the `engine` and `id` of this controller. + * + * This method is called by FlutterEngine. + */ +- (void)detachFromEngine; + @end // Private methods made visible for testing @interface FlutterViewController (TestMethods) - (void)onAccessibilityStatusChanged:(BOOL)enabled; + +- (nonnull FlutterView*)createFlutterViewWithMTLDevice:(nonnull id)device + commandQueue:(nonnull id)commandQueue; + @end diff --git a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h index 98aca04055d5b..f873c72266a95 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterViewProvider.h @@ -4,6 +4,8 @@ #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" +extern const uint64_t kFlutterDefaultViewId; + /** * An interface to query FlutterView. *