1// Copyright 2013 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 "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h" 6#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h" 7 8#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" 9#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h" 10#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h" 11#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h" 12#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h" 13#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h" 14#import "flutter/shell/platform/embedder/embedder.h" 15 16namespace { 17 18/// Clipboard plain text format. 19constexpr char kTextPlainFormat[] = "text/plain"; 20 21/** 22 * State tracking for mouse events, to adapt between the events coming from the system and the 23 * events that the embedding API expects. 24 */ 25struct MouseState { 26 /** 27 * Whether or not a kAdd event has been sent (or sent again since the last kRemove if tracking is 28 * enabled). Used to determine whether to send a kAdd event before sending an incoming mouse 29 * event, since Flutter expects pointers to be added before events are sent for them. 30 */ 31 bool flutter_state_is_added = false; 32 33 /** 34 * Whether or not a kDown has been sent since the last kAdd/kUp. 35 */ 36 bool flutter_state_is_down = false; 37 38 /** 39 * Whether or not mouseExited: was received while a button was down. Cocoa's behavior when 40 * dragging out of a tracked area is to send an exit, then keep sending drag events until the last 41 * button is released. If it was released inside the view, mouseEntered: is sent the next time the 42 * mouse moves. Flutter doesn't expect to receive events after a kRemove, so the kRemove for the 43 * exit needs to be delayed until after the last mouse button is released. 44 */ 45 bool has_pending_exit = false; 46 47 /** 48 * The currently pressed buttons, as represented in FlutterPointerEvent. 49 */ 50 int64_t buttons = 0; 51 52 /** 53 * Resets all state to default values. 54 */ 55 void Reset() { 56 flutter_state_is_added = false; 57 flutter_state_is_down = false; 58 has_pending_exit = false; 59 buttons = 0; 60 } 61}; 62 63} // namespace 64 65#pragma mark - Private interface declaration. 66 67/** 68 * Private interface declaration for FlutterViewController. 69 */ 70@interface FlutterViewController () <FlutterViewReshapeListener> 71 72/** 73 * A list of additional responders to keyboard events. Keybord events are forwarded to all of them. 74 */ 75@property(nonatomic) NSMutableOrderedSet<NSResponder*>* additionalKeyResponders; 76 77/** 78 * The tracking area used to generate hover events, if enabled. 79 */ 80@property(nonatomic) NSTrackingArea* trackingArea; 81 82/** 83 * The current state of the mouse and the sent mouse events. 84 */ 85@property(nonatomic) MouseState mouseState; 86 87/** 88 * Starts running |engine|, including any initial setup. 89 */ 90- (BOOL)launchEngine; 91 92/** 93 * Updates |trackingArea| for the current tracking settings, creating it with 94 * the correct mode if tracking is enabled, or removing it if not. 95 */ 96- (void)configureTrackingArea; 97 98/** 99 * Creates and registers plugins used by this view controller. 100 */ 101- (void)addInternalPlugins; 102 103/** 104 * Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState. 105 * 106 * mouseState.buttons should be updated before calling this method. 107 */ 108- (void)dispatchMouseEvent:(nonnull NSEvent*)event; 109 110/** 111 * Converts |event| to a FlutterPointerEvent with the given phase, and sends it to the engine. 112 */ 113- (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase; 114 115/** 116 * Converts |event| to a key event channel message, and sends it to the engine. 117 */ 118- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type; 119 120/** 121 * Initializes the KVO for user settings and passes the initial user settings to the engine. 122 */ 123- (void)sendInitialSettings; 124 125/** 126 * Responsds to updates in the user settings and passes this data to the engine. 127 */ 128- (void)onSettingsChanged:(NSNotification*)notification; 129 130/** 131 * Handles messages received from the Flutter engine on the _*Channel channels. 132 */ 133- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; 134 135/** 136 * Reads the data from the clipboard. |format| specifies the media type of the 137 * data to obtain. 138 */ 139- (NSDictionary*)getClipboardData:(NSString*)format; 140 141/** 142 * Clears contents and writes new data into clipboard. |data| is a dictionary where 143 * the keys are the type of data, and tervalue the data to be stored. 144 */ 145- (void)setClipboardData:(NSDictionary*)data; 146 147@end 148 149#pragma mark - FlutterViewController implementation. 150 151@implementation FlutterViewController { 152 // The project to run in this controller's engine. 153 FlutterDartProject* _project; 154 155 // The plugin used to handle text input. This is not an FlutterPlugin, so must be owned 156 // separately. 157 FlutterTextInputPlugin* _textInputPlugin; 158 159 // A message channel for passing key events to the Flutter engine. This should be replaced with 160 // an embedding API; see Issue #47. 161 FlutterBasicMessageChannel* _keyEventChannel; 162 163 // A message channel for sending user settings to the flutter engine. 164 FlutterBasicMessageChannel* _settingsChannel; 165 166 // A method channel for miscellaneous platform functionality. 167 FlutterMethodChannel* _platformChannel; 168} 169 170@dynamic view; 171 172/** 173 * Performs initialization that's common between the different init paths. 174 */ 175static void CommonInit(FlutterViewController* controller) { 176 controller->_engine = [[FlutterEngine alloc] initWithName:@"io.flutter" 177 project:controller->_project 178 allowHeadlessExecution:NO]; 179 controller->_additionalKeyResponders = [[NSMutableOrderedSet alloc] init]; 180 controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow; 181} 182 183- (instancetype)initWithCoder:(NSCoder*)coder { 184 self = [super initWithCoder:coder]; 185 NSAssert(self, @"Super init cannot be nil"); 186 187 CommonInit(self); 188 return self; 189} 190 191- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { 192 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 193 NSAssert(self, @"Super init cannot be nil"); 194 195 CommonInit(self); 196 return self; 197} 198 199- (instancetype)initWithProject:(nullable FlutterDartProject*)project { 200 self = [super initWithNibName:nil bundle:nil]; 201 NSAssert(self, @"Super init cannot be nil"); 202 203 _project = project; 204 CommonInit(self); 205 return self; 206} 207 208- (void)loadView { 209 NSOpenGLContext* resourceContext = _engine.resourceContext; 210 if (!resourceContext) { 211 NSLog(@"Unable to create FlutterView; no resource context available."); 212 return; 213 } 214 FlutterView* flutterView = [[FlutterView alloc] initWithShareContext:resourceContext 215 reshapeListener:self]; 216 self.view = flutterView; 217} 218 219- (void)viewDidLoad { 220 [self configureTrackingArea]; 221} 222 223- (void)viewWillAppear { 224 [super viewWillAppear]; 225 if (!_engine.running) { 226 [self launchEngine]; 227 } 228} 229 230- (void)dealloc { 231 _engine.viewController = nil; 232} 233 234#pragma mark - Public methods 235 236- (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode { 237 if (_mouseTrackingMode == mode) { 238 return; 239 } 240 _mouseTrackingMode = mode; 241 [self configureTrackingArea]; 242} 243 244#pragma mark - Framework-internal methods 245 246- (FlutterView*)flutterView { 247 return static_cast<FlutterView*>(self.view); 248} 249 250- (void)addKeyResponder:(NSResponder*)responder { 251 [self.additionalKeyResponders addObject:responder]; 252} 253 254- (void)removeKeyResponder:(NSResponder*)responder { 255 [self.additionalKeyResponders removeObject:responder]; 256} 257 258#pragma mark - Private methods 259 260- (BOOL)launchEngine { 261 // Register internal plugins before starting the engine. 262 [self addInternalPlugins]; 263 264 _engine.viewController = self; 265 if (![_engine runWithEntrypoint:nil]) { 266 return NO; 267 } 268 // Send the initial user settings such as brightness and text scale factor 269 // to the engine. 270 [self sendInitialSettings]; 271 return YES; 272} 273 274- (void)configureTrackingArea { 275 if (_mouseTrackingMode != FlutterMouseTrackingModeNone && self.view) { 276 NSTrackingAreaOptions options = 277 NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect; 278 switch (_mouseTrackingMode) { 279 case FlutterMouseTrackingModeInKeyWindow: 280 options |= NSTrackingActiveInKeyWindow; 281 break; 282 case FlutterMouseTrackingModeInActiveApp: 283 options |= NSTrackingActiveInActiveApp; 284 break; 285 case FlutterMouseTrackingModeAlways: 286 options |= NSTrackingActiveAlways; 287 break; 288 default: 289 NSLog(@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode); 290 return; 291 } 292 _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect 293 options:options 294 owner:self 295 userInfo:nil]; 296 [self.view addTrackingArea:_trackingArea]; 297 } else if (_trackingArea) { 298 [self.view removeTrackingArea:_trackingArea]; 299 _trackingArea = nil; 300 } 301} 302 303- (void)addInternalPlugins { 304 _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:self]; 305 _keyEventChannel = 306 [FlutterBasicMessageChannel messageChannelWithName:@"flutter/keyevent" 307 binaryMessenger:_engine.binaryMessenger 308 codec:[FlutterJSONMessageCodec sharedInstance]]; 309 _settingsChannel = 310 [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings" 311 binaryMessenger:_engine.binaryMessenger 312 codec:[FlutterJSONMessageCodec sharedInstance]]; 313 _platformChannel = 314 [FlutterMethodChannel methodChannelWithName:@"flutter/platform" 315 binaryMessenger:_engine.binaryMessenger 316 codec:[FlutterJSONMethodCodec sharedInstance]]; 317 __weak FlutterViewController* weakSelf = self; 318 [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { 319 [weakSelf handleMethodCall:call result:result]; 320 }]; 321} 322 323- (void)dispatchMouseEvent:(nonnull NSEvent*)event { 324 FlutterPointerPhase phase = _mouseState.buttons == 0 325 ? (_mouseState.flutter_state_is_down ? kUp : kHover) 326 : (_mouseState.flutter_state_is_down ? kMove : kDown); 327 [self dispatchMouseEvent:event phase:phase]; 328} 329 330- (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase { 331 // There are edge cases where the system will deliver enter out of order relative to other 332 // events (e.g., drag out and back in, release, then click; mouseDown: will be called before 333 // mouseEntered:). Discard those events, since the add will already have been synthesized. 334 if (_mouseState.flutter_state_is_added && phase == kAdd) { 335 return; 336 } 337 338 // If a pointer added event hasn't been sent, synthesize one using this event for the basic 339 // information. 340 if (!_mouseState.flutter_state_is_added && phase != kAdd) { 341 // Only the values extracted for use in flutterEvent below matter, the rest are dummy values. 342 NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered 343 location:event.locationInWindow 344 modifierFlags:0 345 timestamp:event.timestamp 346 windowNumber:event.windowNumber 347 context:nil 348 eventNumber:0 349 trackingNumber:0 350 userData:NULL]; 351 [self dispatchMouseEvent:addEvent phase:kAdd]; 352 } 353 354 NSPoint locationInView = [self.view convertPoint:event.locationInWindow fromView:nil]; 355 NSPoint locationInBackingCoordinates = [self.view convertPointToBacking:locationInView]; 356 FlutterPointerEvent flutterEvent = { 357 .struct_size = sizeof(flutterEvent), 358 .device_kind = kFlutterPointerDeviceKindMouse, 359 .phase = phase, 360 .x = locationInBackingCoordinates.x, 361 .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative. 362 .timestamp = static_cast<size_t>(event.timestamp * NSEC_PER_MSEC), 363 // If a click triggered a synthesized kAdd, don't pass the buttons in that event. 364 .buttons = phase == kAdd ? 0 : _mouseState.buttons, 365 }; 366 367 if (event.type == NSEventTypeScrollWheel) { 368 flutterEvent.signal_kind = kFlutterPointerSignalKindScroll; 369 370 double pixelsPerLine = 1.0; 371 if (!event.hasPreciseScrollingDeltas) { 372 CGEventSourceRef source = CGEventCreateSourceFromEvent(event.CGEvent); 373 pixelsPerLine = CGEventSourceGetPixelsPerLine(source); 374 if (source) { 375 CFRelease(source); 376 } 377 } 378 double scaleFactor = self.view.layer.contentsScale; 379 flutterEvent.scroll_delta_x = -event.scrollingDeltaX * pixelsPerLine * scaleFactor; 380 flutterEvent.scroll_delta_y = -event.scrollingDeltaY * pixelsPerLine * scaleFactor; 381 } 382 [_engine sendPointerEvent:flutterEvent]; 383 384 // Update tracking of state as reported to Flutter. 385 if (phase == kDown) { 386 _mouseState.flutter_state_is_down = true; 387 } else if (phase == kUp) { 388 _mouseState.flutter_state_is_down = false; 389 if (_mouseState.has_pending_exit) { 390 [self dispatchMouseEvent:event phase:kRemove]; 391 _mouseState.has_pending_exit = false; 392 } 393 } else if (phase == kAdd) { 394 _mouseState.flutter_state_is_added = true; 395 } else if (phase == kRemove) { 396 _mouseState.Reset(); 397 } 398} 399 400- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type { 401 [_keyEventChannel sendMessage:@{ 402 @"keymap" : @"macos", 403 @"type" : type, 404 @"keyCode" : @(event.keyCode), 405 @"modifiers" : @(event.modifierFlags), 406 @"characters" : event.characters, 407 @"charactersIgnoringModifiers" : event.charactersIgnoringModifiers, 408 }]; 409} 410 411- (void)onSettingsChanged:(NSNotification*)notification { 412 // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015. 413 NSString* brightness = 414 [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; 415 [_settingsChannel sendMessage:@{ 416 @"platformBrightness" : [brightness isEqualToString:@"Dark"] ? @"dark" : @"light", 417 // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32006. 418 @"textScaleFactor" : @1.0, 419 @"alwaysUse24HourFormat" : @false 420 }]; 421} 422 423- (void)sendInitialSettings { 424 // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015. 425 [[NSDistributedNotificationCenter defaultCenter] 426 addObserver:self 427 selector:@selector(onSettingsChanged:) 428 name:@"AppleInterfaceThemeChangedNotification" 429 object:nil]; 430 [self onSettingsChanged:nil]; 431} 432 433- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { 434 if ([call.method isEqualToString:@"SystemNavigator.pop"]) { 435 [NSApp terminate:self]; 436 result(nil); 437 } else if ([call.method isEqualToString:@"Clipboard.getData"]) { 438 result([self getClipboardData:call.arguments]); 439 } else if ([call.method isEqualToString:@"Clipboard.setData"]) { 440 [self setClipboardData:call.arguments]; 441 result(nil); 442 } else { 443 result(FlutterMethodNotImplemented); 444 } 445} 446 447- (NSDictionary*)getClipboardData:(NSString*)format { 448 NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; 449 if ([format isEqualToString:@(kTextPlainFormat)]) { 450 NSString* stringInPasteboard = [pasteboard stringForType:NSPasteboardTypeString]; 451 return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard}; 452 } 453 return nil; 454} 455 456- (void)setClipboardData:(NSDictionary*)data { 457 NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; 458 NSString* text = data[@"text"]; 459 if (text && ![text isEqual:[NSNull null]]) { 460 [pasteboard clearContents]; 461 [pasteboard setString:text forType:NSPasteboardTypeString]; 462 } 463} 464 465#pragma mark - FlutterViewReshapeListener 466 467/** 468 * Responds to view reshape by notifying the engine of the change in dimensions. 469 */ 470- (void)viewDidReshape:(NSView*)view { 471 [_engine updateWindowMetrics]; 472} 473 474#pragma mark - FlutterPluginRegistry 475 476- (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName { 477 return [_engine registrarForPlugin:pluginName]; 478} 479 480#pragma mark - NSResponder 481 482- (BOOL)acceptsFirstResponder { 483 return YES; 484} 485 486- (void)keyDown:(NSEvent*)event { 487 [self dispatchKeyEvent:event ofType:@"keydown"]; 488 for (NSResponder* responder in self.additionalKeyResponders) { 489 if ([responder respondsToSelector:@selector(keyDown:)]) { 490 [responder keyDown:event]; 491 } 492 } 493} 494 495- (void)keyUp:(NSEvent*)event { 496 [self dispatchKeyEvent:event ofType:@"keyup"]; 497 for (NSResponder* responder in self.additionalKeyResponders) { 498 if ([responder respondsToSelector:@selector(keyUp:)]) { 499 [responder keyUp:event]; 500 } 501 } 502} 503 504- (void)mouseEntered:(NSEvent*)event { 505 [self dispatchMouseEvent:event phase:kAdd]; 506} 507 508- (void)mouseExited:(NSEvent*)event { 509 if (_mouseState.buttons != 0) { 510 _mouseState.has_pending_exit = true; 511 return; 512 } 513 [self dispatchMouseEvent:event phase:kRemove]; 514} 515 516- (void)mouseDown:(NSEvent*)event { 517 _mouseState.buttons |= kFlutterPointerButtonMousePrimary; 518 [self dispatchMouseEvent:event]; 519} 520 521- (void)mouseUp:(NSEvent*)event { 522 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary); 523 [self dispatchMouseEvent:event]; 524} 525 526- (void)mouseDragged:(NSEvent*)event { 527 [self dispatchMouseEvent:event]; 528} 529 530- (void)rightMouseDown:(NSEvent*)event { 531 _mouseState.buttons |= kFlutterPointerButtonMouseSecondary; 532 [self dispatchMouseEvent:event]; 533} 534 535- (void)rightMouseUp:(NSEvent*)event { 536 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary); 537 [self dispatchMouseEvent:event]; 538} 539 540- (void)rightMouseDragged:(NSEvent*)event { 541 [self dispatchMouseEvent:event]; 542} 543 544- (void)otherMouseDown:(NSEvent*)event { 545 _mouseState.buttons |= (1 << event.buttonNumber); 546 [self dispatchMouseEvent:event]; 547} 548 549- (void)otherMouseUp:(NSEvent*)event { 550 _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber); 551 [self dispatchMouseEvent:event]; 552} 553 554- (void)otherMouseDragged:(NSEvent*)event { 555 [self dispatchMouseEvent:event]; 556} 557 558- (void)mouseMoved:(NSEvent*)event { 559 [self dispatchMouseEvent:event]; 560} 561 562- (void)scrollWheel:(NSEvent*)event { 563 // TODO: Add gesture-based (trackpad) scroll support once it's supported by the engine rather 564 // than always using kHover. 565 [self dispatchMouseEvent:event phase:kHover]; 566} 567 568@end 569