Flutter macOS Embedder
FlutterViewController.mm
Go to the documentation of this file.
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 
7 
8 #include <Carbon/Carbon.h>
9 #import <objc/message.h>
10 
11 #include "flutter/common/constants.h"
12 #include "flutter/shell/platform/embedder/embedder.h"
13 
23 
24 #pragma mark - Static types and data.
25 
26 namespace {
29 
30 // Use different device ID for mouse and pan/zoom events, since we can't differentiate the actual
31 // device (mouse v.s. trackpad).
32 static constexpr int32_t kMousePointerDeviceId = 0;
33 static constexpr int32_t kPointerPanZoomDeviceId = 1;
34 
35 // A trackpad touch following inertial scrolling should cause an inertia cancel
36 // event to be issued. Use a window of 50 milliseconds after the scroll to account
37 // for delays in event propagation observed in macOS Ventura.
38 static constexpr double kTrackpadTouchInertiaCancelWindowMs = 0.050;
39 
40 /**
41  * State tracking for mouse events, to adapt between the events coming from the system and the
42  * events that the embedding API expects.
43  */
44 struct MouseState {
45  /**
46  * The currently pressed buttons, as represented in FlutterPointerEvent.
47  */
48  int64_t buttons = 0;
49 
50  /**
51  * The accumulated gesture pan.
52  */
53  CGFloat delta_x = 0;
54  CGFloat delta_y = 0;
55 
56  /**
57  * The accumulated gesture zoom scale.
58  */
59  CGFloat scale = 0;
60 
61  /**
62  * The accumulated gesture rotation.
63  */
64  CGFloat rotation = 0;
65 
66  /**
67  * Whether or not a kAdd event has been sent (or sent again since the last kRemove if tracking is
68  * enabled). Used to determine whether to send a kAdd event before sending an incoming mouse
69  * event, since Flutter expects pointers to be added before events are sent for them.
70  */
71  bool flutter_state_is_added = false;
72 
73  /**
74  * Whether or not a kDown has been sent since the last kAdd/kUp.
75  */
76  bool flutter_state_is_down = false;
77 
78  /**
79  * Whether or not mouseExited: was received while a button was down. Cocoa's behavior when
80  * dragging out of a tracked area is to send an exit, then keep sending drag events until the last
81  * button is released. Flutter doesn't expect to receive events after a kRemove, so the kRemove
82  * for the exit needs to be delayed until after the last mouse button is released. If cursor
83  * returns back to the window while still dragging, the flag is cleared in mouseEntered:.
84  */
85  bool has_pending_exit = false;
86 
87  /*
88  * Whether or not a kPanZoomStart has been sent since the last kAdd/kPanZoomEnd.
89  */
90  bool flutter_state_is_pan_zoom_started = false;
91 
92  /**
93  * State of pan gesture.
94  */
95  NSEventPhase pan_gesture_phase = NSEventPhaseNone;
96 
97  /**
98  * State of scale gesture.
99  */
100  NSEventPhase scale_gesture_phase = NSEventPhaseNone;
101 
102  /**
103  * State of rotate gesture.
104  */
105  NSEventPhase rotate_gesture_phase = NSEventPhaseNone;
106 
107  /**
108  * Time of last scroll momentum event.
109  */
110  NSTimeInterval last_scroll_momentum_changed_time = 0;
111 
112  /**
113  * Resets all gesture state to default values.
114  */
115  void GestureReset() {
116  delta_x = 0;
117  delta_y = 0;
118  scale = 0;
119  rotation = 0;
120  flutter_state_is_pan_zoom_started = false;
121  pan_gesture_phase = NSEventPhaseNone;
122  scale_gesture_phase = NSEventPhaseNone;
123  rotate_gesture_phase = NSEventPhaseNone;
124  }
125 
126  /**
127  * Resets all state to default values.
128  */
129  void Reset() {
130  flutter_state_is_added = false;
131  flutter_state_is_down = false;
132  has_pending_exit = false;
133  buttons = 0;
134  }
135 };
136 
137 } // namespace
138 
139 #pragma mark - Private interface declaration.
140 
141 /**
142  * FlutterViewWrapper is a convenience class that wraps a FlutterView and provides
143  * a mechanism to attach AppKit views such as FlutterTextField without affecting
144  * the accessibility subtree of the wrapped FlutterView itself.
145  *
146  * The FlutterViewController uses this class to create its content view. When
147  * any of the accessibility services (e.g. VoiceOver) is turned on, the accessibility
148  * bridge creates FlutterTextFields that interact with the service. The bridge has to
149  * attach the FlutterTextField somewhere in the view hierarchy in order for the
150  * FlutterTextField to interact correctly with VoiceOver. Those FlutterTextFields
151  * will be attached to this view so that they won't affect the accessibility subtree
152  * of FlutterView.
153  */
154 @interface FlutterViewWrapper : NSView
155 
156 - (void)setBackgroundColor:(NSColor*)color;
157 
158 @end
159 
160 /**
161  * Private interface declaration for FlutterViewController.
162  */
164 
165 /**
166  * The tracking area used to generate hover events, if enabled.
167  */
168 @property(nonatomic) NSTrackingArea* trackingArea;
169 
170 /**
171  * The current state of the mouse and the sent mouse events.
172  */
173 @property(nonatomic) MouseState mouseState;
174 
175 /**
176  * Event monitor for keyUp events.
177  */
178 @property(nonatomic) id keyUpMonitor;
179 
180 /**
181  * Pointer to a keyboard manager, a hub that manages how key events are
182  * dispatched to various Flutter key responders, and whether the event is
183  * propagated to the next NSResponder.
184  */
185 @property(nonatomic, readonly, nonnull) FlutterKeyboardManager* keyboardManager;
186 
187 @property(nonatomic) KeyboardLayoutNotifier keyboardLayoutNotifier;
188 
189 @property(nonatomic) NSData* keyboardLayoutData;
190 
191 /**
192  * Starts running |engine|, including any initial setup.
193  */
194 - (BOOL)launchEngine;
195 
196 /**
197  * Updates |trackingArea| for the current tracking settings, creating it with
198  * the correct mode if tracking is enabled, or removing it if not.
199  */
200 - (void)configureTrackingArea;
201 
202 /**
203  * Creates and registers keyboard related components.
204  */
205 - (void)initializeKeyboard;
206 
207 /**
208  * Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState.
209  *
210  * mouseState.buttons should be updated before calling this method.
211  */
212 - (void)dispatchMouseEvent:(nonnull NSEvent*)event;
213 
214 /**
215  * Calls dispatchMouseEvent:phase: with a phase determined by event.phase.
216  */
217 - (void)dispatchGestureEvent:(nonnull NSEvent*)event;
218 
219 /**
220  * Converts |event| to a FlutterPointerEvent with the given phase, and sends it to the engine.
221  */
222 - (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;
223 
224 /**
225  * Called when the active keyboard input source changes.
226  *
227  * Input sources may be simple keyboard layouts, or more complex input methods involving an IME,
228  * such as Chinese, Japanese, and Korean.
229  */
230 - (void)onKeyboardLayoutChanged;
231 
232 @end
233 
234 #pragma mark - FlutterViewWrapper implementation.
235 
236 /**
237  * NotificationCenter callback invoked on kTISNotifySelectedKeyboardInputSourceChanged events.
238  */
239 static void OnKeyboardLayoutChanged(CFNotificationCenterRef center,
240  void* observer,
241  CFStringRef name,
242  const void* object,
243  CFDictionaryRef userInfo) {
244  FlutterViewController* controller = (__bridge FlutterViewController*)observer;
245  if (controller != nil) {
246  [controller onKeyboardLayoutChanged];
247  }
248 }
249 
250 @implementation FlutterViewWrapper {
251  FlutterView* _flutterView;
253 }
254 
255 - (instancetype)initWithFlutterView:(FlutterView*)view
256  controller:(FlutterViewController*)controller {
257  self = [super initWithFrame:NSZeroRect];
258  if (self) {
259  _flutterView = view;
260  _controller = controller;
261  view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
262  [self addSubview:view];
263  }
264  return self;
265 }
266 
267 - (void)setBackgroundColor:(NSColor*)color {
268  [_flutterView setBackgroundColor:color];
269 }
270 
271 - (BOOL)performKeyEquivalent:(NSEvent*)event {
272  // Do not intercept the event if flutterView is not first responder, otherwise this would
273  // interfere with TextInputPlugin, which also handles key equivalents.
274  //
275  // Also do not intercept the event if key equivalent is a product of an event being
276  // redispatched by the TextInputPlugin, in which case it needs to bubble up so that menus
277  // can handle key equivalents.
278  if (self.window.firstResponder != _flutterView || [_controller isDispatchingKeyEvent:event]) {
279  return [super performKeyEquivalent:event];
280  }
281  [_flutterView keyDown:event];
282  return YES;
283 }
284 
285 - (NSArray*)accessibilityChildren {
286  return @[ _flutterView ];
287 }
288 
289 // TODO(cbracken): https://github.com/flutter/flutter/issues/154063
290 // Remove this whole method override when we drop support for macOS 12 (Monterey).
291 - (void)mouseDown:(NSEvent*)event {
292  if (@available(macOS 13.3.1, *)) {
293  [super mouseDown:event];
294  } else {
295  // Work around an AppKit bug where mouseDown/mouseUp are not called on the view controller if
296  // the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
297  // setting is enabled.
298  //
299  // This simply calls mouseDown on the next responder in the responder chain as the default
300  // implementation on NSResponder is documented to do.
301  //
302  // See: https://github.com/flutter/flutter/issues/115015
303  // See: http://www.openradar.me/FB12050037
304  // See: https://developer.apple.com/documentation/appkit/nsresponder/1524634-mousedown
305  [self.nextResponder mouseDown:event];
306  }
307 }
308 
309 // TODO(cbracken): https://github.com/flutter/flutter/issues/154063
310 // Remove this workaround when we drop support for macOS 12 (Monterey).
311 - (void)mouseUp:(NSEvent*)event {
312  if (@available(macOS 13.3.1, *)) {
313  [super mouseUp:event];
314  } else {
315  // Work around an AppKit bug where mouseDown/mouseUp are not called on the view controller if
316  // the view is the content view of an NSPopover AND macOS's Reduced Transparency accessibility
317  // setting is enabled.
318  //
319  // This simply calls mouseUp on the next responder in the responder chain as the default
320  // implementation on NSResponder is documented to do.
321  //
322  // See: https://github.com/flutter/flutter/issues/115015
323  // See: http://www.openradar.me/FB12050037
324  // See: https://developer.apple.com/documentation/appkit/nsresponder/1535349-mouseup
325  [self.nextResponder mouseUp:event];
326  }
327 }
328 
329 @end
330 
331 #pragma mark - FlutterViewController implementation.
332 
333 @implementation FlutterViewController {
334  // The project to run in this controller's engine.
336 
337  std::shared_ptr<flutter::AccessibilityBridgeMac> _bridge;
338 
339  // FlutterViewController does not actually uses the synchronizer, but only
340  // passes it to FlutterView.
342 }
343 
344 @synthesize viewIdentifier = _viewIdentifier;
345 @dynamic accessibilityBridge;
346 
347 /**
348  * Performs initialization that's common between the different init paths.
349  */
350 static void CommonInit(FlutterViewController* controller, FlutterEngine* engine) {
351  if (!engine) {
352  engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
353  project:controller->_project
354  allowHeadlessExecution:NO];
355  }
356  NSCAssert(controller.engine == nil,
357  @"The FlutterViewController is unexpectedly attached to "
358  @"engine %@ before initialization.",
359  controller.engine);
360  [engine addViewController:controller];
361  NSCAssert(controller.engine != nil,
362  @"The FlutterViewController unexpectedly stays unattached after initialization. "
363  @"In unit tests, this is likely because either the FlutterViewController or "
364  @"the FlutterEngine is mocked. Please subclass these classes instead.",
365  controller.engine, controller.viewIdentifier);
366  controller->_mouseTrackingMode = kFlutterMouseTrackingModeInKeyWindow;
367  controller->_textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:controller];
368  [controller initializeKeyboard];
369  [controller notifySemanticsEnabledChanged];
370  // macOS fires this message when changing IMEs.
371  CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter();
372  __weak FlutterViewController* weakSelf = controller;
373  CFNotificationCenterAddObserver(cfCenter, (__bridge void*)weakSelf, OnKeyboardLayoutChanged,
374  kTISNotifySelectedKeyboardInputSourceChanged, NULL,
375  CFNotificationSuspensionBehaviorDeliverImmediately);
376 }
377 
378 - (instancetype)initWithCoder:(NSCoder*)coder {
379  self = [super initWithCoder:coder];
380  NSAssert(self, @"Super init cannot be nil");
381 
382  CommonInit(self, nil);
383  return self;
384 }
385 
386 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
387  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
388  NSAssert(self, @"Super init cannot be nil");
389 
390  CommonInit(self, nil);
391  return self;
392 }
393 
394 - (instancetype)initWithProject:(nullable FlutterDartProject*)project {
395  self = [super initWithNibName:nil bundle:nil];
396  NSAssert(self, @"Super init cannot be nil");
397 
398  _project = project;
399  CommonInit(self, nil);
400  return self;
401 }
402 
403 - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine
404  nibName:(nullable NSString*)nibName
405  bundle:(nullable NSBundle*)nibBundle {
406  NSAssert(engine != nil, @"Engine is required");
407 
408  self = [super initWithNibName:nibName bundle:nibBundle];
409  if (self) {
410  CommonInit(self, engine);
411  }
412 
413  return self;
414 }
415 
416 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
417  return [_keyboardManager isDispatchingKeyEvent:event];
418 }
419 
420 - (void)loadView {
421  FlutterView* flutterView;
422  id<MTLDevice> device = _engine.renderer.device;
423  id<MTLCommandQueue> commandQueue = _engine.renderer.commandQueue;
424  if (!device || !commandQueue) {
425  NSLog(@"Unable to create FlutterView; no MTLDevice or MTLCommandQueue available.");
426  return;
427  }
428  flutterView = [self createFlutterViewWithMTLDevice:device commandQueue:commandQueue];
429  if (_backgroundColor != nil) {
430  [flutterView setBackgroundColor:_backgroundColor];
431  }
432  FlutterViewWrapper* wrapperView = [[FlutterViewWrapper alloc] initWithFlutterView:flutterView
433  controller:self];
434  self.view = wrapperView;
435  _flutterView = flutterView;
436 }
437 
438 - (void)viewDidLoad {
439  [self configureTrackingArea];
440  [self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
441  [self.view setWantsRestingTouches:YES];
442  [_engine viewControllerViewDidLoad:self];
443 }
444 
445 - (void)viewWillAppear {
446  [super viewWillAppear];
447  if (!_engine.running) {
448  [self launchEngine];
449  }
450  [self listenForMetaModifiedKeyUpEvents];
451 }
452 
453 - (void)viewWillDisappear {
454  // Per Apple's documentation, it is discouraged to call removeMonitor: in dealloc, and it's
455  // recommended to be called earlier in the lifecycle.
456  [NSEvent removeMonitor:_keyUpMonitor];
457  _keyUpMonitor = nil;
458 }
459 
460 - (void)dealloc {
461  if ([self attached]) {
462  [_engine removeViewController:self];
463  }
464  CFNotificationCenterRef cfCenter = CFNotificationCenterGetDistributedCenter();
465  CFNotificationCenterRemoveEveryObserver(cfCenter, (__bridge void*)self);
466 }
467 
468 #pragma mark - Public methods
469 
470 - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
471  if (_mouseTrackingMode == mode) {
472  return;
473  }
474  _mouseTrackingMode = mode;
475  [self configureTrackingArea];
476 }
477 
478 - (void)setBackgroundColor:(NSColor*)color {
479  _backgroundColor = color;
480  [_flutterView setBackgroundColor:_backgroundColor];
481 }
482 
483 - (FlutterViewIdentifier)viewIdentifier {
484  NSAssert([self attached], @"This view controller is not attached.");
485  return _viewIdentifier;
486 }
487 
488 - (void)onPreEngineRestart {
489  [self initializeKeyboard];
490 }
491 
492 - (void)notifySemanticsEnabledChanged {
493  BOOL mySemanticsEnabled = !!_bridge;
494  BOOL newSemanticsEnabled = _engine.semanticsEnabled;
495  if (newSemanticsEnabled == mySemanticsEnabled) {
496  return;
497  }
498  if (newSemanticsEnabled) {
499  _bridge = [self createAccessibilityBridgeWithEngine:_engine];
500  } else {
501  // Remove the accessibility children from flutter view before resetting the bridge.
502  _flutterView.accessibilityChildren = nil;
503  _bridge.reset();
504  }
505  NSAssert(newSemanticsEnabled == !!_bridge, @"Failed to update semantics for the view.");
506 }
507 
508 - (std::weak_ptr<flutter::AccessibilityBridgeMac>)accessibilityBridge {
509  return _bridge;
510 }
511 
512 - (void)setUpWithEngine:(FlutterEngine*)engine
513  viewIdentifier:(FlutterViewIdentifier)viewIdentifier
514  threadSynchronizer:(FlutterThreadSynchronizer*)threadSynchronizer {
515  NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine);
516  _engine = engine;
517  _viewIdentifier = viewIdentifier;
518  _threadSynchronizer = threadSynchronizer;
519  [_threadSynchronizer registerView:_viewIdentifier];
520 }
521 
522 - (void)detachFromEngine {
523  NSAssert(_engine != nil, @"Not attached to any engine.");
524  [_threadSynchronizer deregisterView:_viewIdentifier];
525  _threadSynchronizer = nil;
526  _engine = nil;
527 }
528 
529 - (BOOL)attached {
530  return _engine != nil;
531 }
532 
533 - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
534  // Semantics will be disabled when unfocusing application but the updateSemantics:
535  // callback is received in next run loop turn.
536  if (!_engine.semanticsEnabled) {
537  return;
538  }
539  for (size_t i = 0; i < update->node_count; i++) {
540  const FlutterSemanticsNode2* node = update->nodes[i];
541  _bridge->AddFlutterSemanticsNodeUpdate(*node);
542  }
543 
544  for (size_t i = 0; i < update->custom_action_count; i++) {
545  const FlutterSemanticsCustomAction2* action = update->custom_actions[i];
546  _bridge->AddFlutterSemanticsCustomActionUpdate(*action);
547  }
548 
549  _bridge->CommitUpdates();
550 
551  // Accessibility tree can only be used when the view is loaded.
552  if (!self.viewLoaded) {
553  return;
554  }
555  // Attaches the accessibility root to the flutter view.
556  auto root = _bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
557  if (root) {
558  if ([self.flutterView.accessibilityChildren count] == 0) {
559  NSAccessibilityElement* native_root = root->GetNativeViewAccessible();
560  self.flutterView.accessibilityChildren = @[ native_root ];
561  }
562  } else {
563  self.flutterView.accessibilityChildren = nil;
564  }
565 }
566 
567 #pragma mark - Private methods
568 
569 - (BOOL)launchEngine {
570  if (![_engine runWithEntrypoint:nil]) {
571  return NO;
572  }
573  return YES;
574 }
575 
576 // macOS does not call keyUp: on a key while the command key is pressed. This results in a loss
577 // of a key event once the modified key is released. This method registers the
578 // ViewController as a listener for a keyUp event before it's handled by NSApplication, and should
579 // NOT modify the event to avoid any unexpected behavior.
580 - (void)listenForMetaModifiedKeyUpEvents {
581  if (_keyUpMonitor != nil) {
582  // It is possible for [NSViewController viewWillAppear] to be invoked multiple times
583  // in a row. https://github.com/flutter/flutter/issues/105963
584  return;
585  }
586  FlutterViewController* __weak weakSelf = self;
587  _keyUpMonitor = [NSEvent
588  addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
589  handler:^NSEvent*(NSEvent* event) {
590  // Intercept keyUp only for events triggered on the current
591  // view or textInputPlugin.
592  NSResponder* firstResponder = [[event window] firstResponder];
593  if (weakSelf.viewLoaded && weakSelf.flutterView &&
594  (firstResponder == weakSelf.flutterView ||
595  firstResponder == weakSelf.textInputPlugin) &&
596  ([event modifierFlags] & NSEventModifierFlagCommand) &&
597  ([event type] == NSEventTypeKeyUp)) {
598  [weakSelf keyUp:event];
599  }
600  return event;
601  }];
602 }
603 
604 - (void)configureTrackingArea {
605  if (!self.viewLoaded) {
606  // The viewDidLoad will call configureTrackingArea again when
607  // the view is actually loaded.
608  return;
609  }
610  if (_mouseTrackingMode != kFlutterMouseTrackingModeNone && self.flutterView) {
611  NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
612  NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag;
613  switch (_mouseTrackingMode) {
614  case kFlutterMouseTrackingModeInKeyWindow:
615  options |= NSTrackingActiveInKeyWindow;
616  break;
617  case kFlutterMouseTrackingModeInActiveApp:
618  options |= NSTrackingActiveInActiveApp;
619  break;
620  case kFlutterMouseTrackingModeAlways:
621  options |= NSTrackingActiveAlways;
622  break;
623  default:
624  NSLog(@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
625  return;
626  }
627  _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
628  options:options
629  owner:self
630  userInfo:nil];
631  [self.flutterView addTrackingArea:_trackingArea];
632  } else if (_trackingArea) {
633  [self.flutterView removeTrackingArea:_trackingArea];
634  _trackingArea = nil;
635  }
636 }
637 
638 - (void)initializeKeyboard {
639  // TODO(goderbauer): Seperate keyboard/textinput stuff into ViewController specific and Engine
640  // global parts. Move the global parts to FlutterEngine.
641  _keyboardManager = [[FlutterKeyboardManager alloc] initWithViewDelegate:self];
642 }
643 
644 - (void)dispatchMouseEvent:(nonnull NSEvent*)event {
645  FlutterPointerPhase phase = _mouseState.buttons == 0
646  ? (_mouseState.flutter_state_is_down ? kUp : kHover)
647  : (_mouseState.flutter_state_is_down ? kMove : kDown);
648  [self dispatchMouseEvent:event phase:phase];
649 }
650 
651 - (void)dispatchGestureEvent:(nonnull NSEvent*)event {
652  if (event.phase == NSEventPhaseBegan || event.phase == NSEventPhaseMayBegin) {
653  [self dispatchMouseEvent:event phase:kPanZoomStart];
654  } else if (event.phase == NSEventPhaseChanged) {
655  [self dispatchMouseEvent:event phase:kPanZoomUpdate];
656  } else if (event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled) {
657  [self dispatchMouseEvent:event phase:kPanZoomEnd];
658  } else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
659  [self dispatchMouseEvent:event phase:kHover];
660  } else {
661  // Waiting until the first momentum change event is a workaround for an issue where
662  // touchesBegan: is called unexpectedly while in low power mode within the interval between
663  // momentum start and the first momentum change.
664  if (event.momentumPhase == NSEventPhaseChanged) {
665  _mouseState.last_scroll_momentum_changed_time = event.timestamp;
666  }
667  // Skip momentum update events, the framework will generate scroll momentum.
668  NSAssert(event.momentumPhase != NSEventPhaseNone,
669  @"Received gesture event with unexpected phase");
670  }
671 }
672 
673 - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
674  NSAssert(self.viewLoaded, @"View must be loaded before it handles the mouse event");
675  // There are edge cases where the system will deliver enter out of order relative to other
676  // events (e.g., drag out and back in, release, then click; mouseDown: will be called before
677  // mouseEntered:). Discard those events, since the add will already have been synthesized.
678  if (_mouseState.flutter_state_is_added && phase == kAdd) {
679  return;
680  }
681 
682  // Multiple gesture recognizers could be active at once, we can't send multiple kPanZoomStart.
683  // For example: rotation and magnification.
684  if (phase == kPanZoomStart || phase == kPanZoomEnd) {
685  if (event.type == NSEventTypeScrollWheel) {
686  _mouseState.pan_gesture_phase = event.phase;
687  } else if (event.type == NSEventTypeMagnify) {
688  _mouseState.scale_gesture_phase = event.phase;
689  } else if (event.type == NSEventTypeRotate) {
690  _mouseState.rotate_gesture_phase = event.phase;
691  }
692  }
693  if (phase == kPanZoomStart) {
694  if (event.type == NSEventTypeScrollWheel) {
695  // Ensure scroll inertia cancel event is not sent afterwards.
696  _mouseState.last_scroll_momentum_changed_time = 0;
697  }
698  if (_mouseState.flutter_state_is_pan_zoom_started) {
699  // Already started on a previous gesture type
700  return;
701  }
702  _mouseState.flutter_state_is_pan_zoom_started = true;
703  }
704  if (phase == kPanZoomEnd) {
705  if (!_mouseState.flutter_state_is_pan_zoom_started) {
706  // NSEventPhaseCancelled is sometimes received at incorrect times in the state
707  // machine, just ignore it here if it doesn't make sense
708  // (we have no active gesture to cancel).
709  NSAssert(event.phase == NSEventPhaseCancelled,
710  @"Received gesture event with unexpected phase");
711  return;
712  }
713  // NSEventPhase values are powers of two, we can use this to inspect merged phases.
714  NSEventPhase all_gestures_fields = _mouseState.pan_gesture_phase |
715  _mouseState.scale_gesture_phase |
716  _mouseState.rotate_gesture_phase;
717  NSEventPhase active_mask = NSEventPhaseBegan | NSEventPhaseChanged;
718  if ((all_gestures_fields & active_mask) != 0) {
719  // Even though this gesture type ended, a different type is still active.
720  return;
721  }
722  }
723 
724  // If a pointer added event hasn't been sent, synthesize one using this event for the basic
725  // information.
726  if (!_mouseState.flutter_state_is_added && phase != kAdd) {
727  // Only the values extracted for use in flutterEvent below matter, the rest are dummy values.
728  NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
729  location:event.locationInWindow
730  modifierFlags:0
731  timestamp:event.timestamp
732  windowNumber:event.windowNumber
733  context:nil
734  eventNumber:0
735  trackingNumber:0
736  userData:NULL];
737  [self dispatchMouseEvent:addEvent phase:kAdd];
738  }
739 
740  NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
741  NSPoint locationInBackingCoordinates = [self.flutterView convertPointToBacking:locationInView];
742  int32_t device = kMousePointerDeviceId;
743  FlutterPointerDeviceKind deviceKind = kFlutterPointerDeviceKindMouse;
744  if (phase == kPanZoomStart || phase == kPanZoomUpdate || phase == kPanZoomEnd) {
745  device = kPointerPanZoomDeviceId;
746  deviceKind = kFlutterPointerDeviceKindTrackpad;
747  }
748  FlutterPointerEvent flutterEvent = {
749  .struct_size = sizeof(flutterEvent),
750  .phase = phase,
751  .timestamp = static_cast<size_t>(event.timestamp * USEC_PER_SEC),
752  .x = locationInBackingCoordinates.x,
753  .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative.
754  .device = device,
755  .device_kind = deviceKind,
756  // If a click triggered a synthesized kAdd, don't pass the buttons in that event.
757  .buttons = phase == kAdd ? 0 : _mouseState.buttons,
758  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
759  };
760 
761  if (phase == kPanZoomUpdate) {
762  if (event.type == NSEventTypeScrollWheel) {
763  _mouseState.delta_x += event.scrollingDeltaX * self.flutterView.layer.contentsScale;
764  _mouseState.delta_y += event.scrollingDeltaY * self.flutterView.layer.contentsScale;
765  } else if (event.type == NSEventTypeMagnify) {
766  _mouseState.scale += event.magnification;
767  } else if (event.type == NSEventTypeRotate) {
768  _mouseState.rotation += event.rotation * (-M_PI / 180.0);
769  }
770  flutterEvent.pan_x = _mouseState.delta_x;
771  flutterEvent.pan_y = _mouseState.delta_y;
772  // Scale value needs to be normalized to range 0->infinity.
773  flutterEvent.scale = pow(2.0, _mouseState.scale);
774  flutterEvent.rotation = _mouseState.rotation;
775  } else if (phase == kPanZoomEnd) {
776  _mouseState.GestureReset();
777  } else if (phase != kPanZoomStart && event.type == NSEventTypeScrollWheel) {
778  flutterEvent.signal_kind = kFlutterPointerSignalKindScroll;
779 
780  double pixelsPerLine = 1.0;
781  if (!event.hasPreciseScrollingDeltas) {
782  // The scrollingDelta needs to be multiplied by the line height.
783  // CGEventSourceGetPixelsPerLine() will return 10, which will result in
784  // scrolling that is noticeably slower than in other applications.
785  // Using 40.0 as the multiplier to match Chromium.
786  // See https://source.chromium.org/chromium/chromium/src/+/main:ui/events/cocoa/events_mac.mm
787  pixelsPerLine = 40.0;
788  }
789  double scaleFactor = self.flutterView.layer.contentsScale;
790  // When mouse input is received while shift is pressed (regardless of
791  // any other pressed keys), Mac automatically flips the axis. Other
792  // platforms do not do this, so we flip it back to normalize the input
793  // received by the framework. The keyboard+mouse-scroll mechanism is exposed
794  // in the ScrollBehavior of the framework so developers can customize the
795  // behavior.
796  // At time of change, Apple does not expose any other type of API or signal
797  // that the X/Y axes have been flipped.
798  double scaledDeltaX = -event.scrollingDeltaX * pixelsPerLine * scaleFactor;
799  double scaledDeltaY = -event.scrollingDeltaY * pixelsPerLine * scaleFactor;
800  if (event.modifierFlags & NSShiftKeyMask) {
801  flutterEvent.scroll_delta_x = scaledDeltaY;
802  flutterEvent.scroll_delta_y = scaledDeltaX;
803  } else {
804  flutterEvent.scroll_delta_x = scaledDeltaX;
805  flutterEvent.scroll_delta_y = scaledDeltaY;
806  }
807  }
808 
809  [_keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
810  [_engine sendPointerEvent:flutterEvent];
811 
812  // Update tracking of state as reported to Flutter.
813  if (phase == kDown) {
814  _mouseState.flutter_state_is_down = true;
815  } else if (phase == kUp) {
816  _mouseState.flutter_state_is_down = false;
817  if (_mouseState.has_pending_exit) {
818  [self dispatchMouseEvent:event phase:kRemove];
819  _mouseState.has_pending_exit = false;
820  }
821  } else if (phase == kAdd) {
822  _mouseState.flutter_state_is_added = true;
823  } else if (phase == kRemove) {
824  _mouseState.Reset();
825  }
826 }
827 
828 - (void)onAccessibilityStatusChanged:(BOOL)enabled {
829  if (!enabled && self.viewLoaded && [_textInputPlugin isFirstResponder]) {
830  // Normally TextInputPlugin, when editing, is child of FlutterViewWrapper.
831  // When accessiblity is enabled the TextInputPlugin gets added as an indirect
832  // child to FlutterTextField. When disabling the plugin needs to be reparented
833  // back.
834  [self.view addSubview:_textInputPlugin];
835  }
836 }
837 
838 - (std::shared_ptr<flutter::AccessibilityBridgeMac>)createAccessibilityBridgeWithEngine:
839  (nonnull FlutterEngine*)engine {
840  return std::make_shared<flutter::AccessibilityBridgeMac>(engine, self);
841 }
842 
843 - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id<MTLDevice>)device
844  commandQueue:(id<MTLCommandQueue>)commandQueue {
845  return [[FlutterView alloc] initWithMTLDevice:device
846  commandQueue:commandQueue
847  delegate:self
848  threadSynchronizer:_threadSynchronizer
849  viewIdentifier:_viewIdentifier];
850 }
851 
852 - (void)onKeyboardLayoutChanged {
853  _keyboardLayoutData = nil;
854  if (_keyboardLayoutNotifier != nil) {
856  }
857 }
858 
859 - (NSString*)lookupKeyForAsset:(NSString*)asset {
860  return [FlutterDartProject lookupKeyForAsset:asset];
861 }
862 
863 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
864  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
865 }
866 
867 #pragma mark - FlutterViewDelegate
868 
869 /**
870  * Responds to view reshape by notifying the engine of the change in dimensions.
871  */
872 - (void)viewDidReshape:(NSView*)view {
873  FML_DCHECK(view == _flutterView);
874  [_engine updateWindowMetricsForViewController:self];
875 }
876 
877 - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view {
878  FML_DCHECK(view == _flutterView);
879  // Only allow FlutterView to become first responder if TextInputPlugin is
880  // not active. Otherwise a mouse event inside FlutterView would cause the
881  // TextInputPlugin to lose first responder status.
882  return !_textInputPlugin.isFirstResponder;
883 }
884 
885 #pragma mark - FlutterPluginRegistry
886 
887 - (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName {
888  return [_engine registrarForPlugin:pluginName];
889 }
890 
891 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
892  return [_engine valuePublishedByPlugin:pluginKey];
893 }
894 
895 #pragma mark - FlutterKeyboardViewDelegate
896 
897 /**
898  * Returns the current Unicode layout data (kTISPropertyUnicodeKeyLayoutData).
899  *
900  * To use the returned data, convert it to CFDataRef first, finds its bytes
901  * with CFDataGetBytePtr, then reinterpret it into const UCKeyboardLayout*.
902  * It's returned in NSData* to enable auto reference count.
903  */
904 static NSData* CurrentKeyboardLayoutData() {
905  TISInputSourceRef source = TISCopyCurrentKeyboardInputSource();
906  CFTypeRef layout_data = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData);
907  if (layout_data == nil) {
908  CFRelease(source);
909  // TISGetInputSourceProperty returns null with Japanese keyboard layout.
910  // Using TISCopyCurrentKeyboardLayoutInputSource to fix NULL return.
911  // https://github.com/microsoft/node-native-keymap/blob/5f0699ded00179410a14c0e1b0e089fe4df8e130/src/keyboard_mac.mm#L91
912  source = TISCopyCurrentKeyboardLayoutInputSource();
913  layout_data = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData);
914  }
915  return (__bridge_transfer NSData*)CFRetain(layout_data);
916 }
917 
918 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
919  callback:(nullable FlutterKeyEventCallback)callback
920  userData:(nullable void*)userData {
921  [_engine sendKeyEvent:event callback:callback userData:userData];
922 }
923 
924 - (id<FlutterBinaryMessenger>)getBinaryMessenger {
925  return _engine.binaryMessenger;
926 }
927 
928 - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
929  return [_textInputPlugin handleKeyEvent:event];
930 }
931 
932 - (void)subscribeToKeyboardLayoutChange:(nullable KeyboardLayoutNotifier)callback {
933  _keyboardLayoutNotifier = callback;
934 }
935 
936 - (LayoutClue)lookUpLayoutForKeyCode:(uint16_t)keyCode shift:(BOOL)shift {
937  if (_keyboardLayoutData == nil) {
938  _keyboardLayoutData = CurrentKeyboardLayoutData();
939  }
940  const UCKeyboardLayout* layout = reinterpret_cast<const UCKeyboardLayout*>(
941  CFDataGetBytePtr((__bridge CFDataRef)_keyboardLayoutData));
942 
943  UInt32 deadKeyState = 0;
944  UniCharCount stringLength = 0;
945  UniChar resultChar;
946 
947  UInt32 modifierState = ((shift ? shiftKey : 0) >> 8) & 0xFF;
948  UInt32 keyboardType = LMGetKbdLast();
949 
950  bool isDeadKey = false;
951  OSStatus status =
952  UCKeyTranslate(layout, keyCode, kUCKeyActionDown, modifierState, keyboardType,
953  kUCKeyTranslateNoDeadKeysBit, &deadKeyState, 1, &stringLength, &resultChar);
954  // For dead keys, press the same key again to get the printable representation of the key.
955  if (status == noErr && stringLength == 0 && deadKeyState != 0) {
956  isDeadKey = true;
957  status =
958  UCKeyTranslate(layout, keyCode, kUCKeyActionDown, modifierState, keyboardType,
959  kUCKeyTranslateNoDeadKeysBit, &deadKeyState, 1, &stringLength, &resultChar);
960  }
961 
962  if (status == noErr && stringLength == 1 && !std::iscntrl(resultChar)) {
963  return LayoutClue{resultChar, isDeadKey};
964  }
965  return LayoutClue{0, false};
966 }
967 
968 - (nonnull NSDictionary*)getPressedState {
969  return [_keyboardManager getPressedState];
970 }
971 
972 #pragma mark - NSResponder
973 
974 - (BOOL)acceptsFirstResponder {
975  return YES;
976 }
977 
978 - (void)keyDown:(NSEvent*)event {
979  [_keyboardManager handleEvent:event];
980 }
981 
982 - (void)keyUp:(NSEvent*)event {
983  [_keyboardManager handleEvent:event];
984 }
985 
986 - (void)flagsChanged:(NSEvent*)event {
987  [_keyboardManager handleEvent:event];
988 }
989 
990 - (void)mouseEntered:(NSEvent*)event {
991  if (_mouseState.has_pending_exit) {
992  _mouseState.has_pending_exit = false;
993  } else {
994  [self dispatchMouseEvent:event phase:kAdd];
995  }
996 }
997 
998 - (void)mouseExited:(NSEvent*)event {
999  if (_mouseState.buttons != 0) {
1000  _mouseState.has_pending_exit = true;
1001  return;
1002  }
1003  [self dispatchMouseEvent:event phase:kRemove];
1004 }
1005 
1006 - (void)mouseDown:(NSEvent*)event {
1007  _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
1008  [self dispatchMouseEvent:event];
1009 }
1010 
1011 - (void)mouseUp:(NSEvent*)event {
1012  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
1013  [self dispatchMouseEvent:event];
1014 }
1015 
1016 - (void)mouseDragged:(NSEvent*)event {
1017  [self dispatchMouseEvent:event];
1018 }
1019 
1020 - (void)rightMouseDown:(NSEvent*)event {
1021  _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
1022  [self dispatchMouseEvent:event];
1023 }
1024 
1025 - (void)rightMouseUp:(NSEvent*)event {
1026  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
1027  [self dispatchMouseEvent:event];
1028 }
1029 
1030 - (void)rightMouseDragged:(NSEvent*)event {
1031  [self dispatchMouseEvent:event];
1032 }
1033 
1034 - (void)otherMouseDown:(NSEvent*)event {
1035  _mouseState.buttons |= (1 << event.buttonNumber);
1036  [self dispatchMouseEvent:event];
1037 }
1038 
1039 - (void)otherMouseUp:(NSEvent*)event {
1040  _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
1041  [self dispatchMouseEvent:event];
1042 }
1043 
1044 - (void)otherMouseDragged:(NSEvent*)event {
1045  [self dispatchMouseEvent:event];
1046 }
1047 
1048 - (void)mouseMoved:(NSEvent*)event {
1049  [self dispatchMouseEvent:event];
1050 }
1051 
1052 - (void)scrollWheel:(NSEvent*)event {
1053  [self dispatchGestureEvent:event];
1054 }
1055 
1056 - (void)magnifyWithEvent:(NSEvent*)event {
1057  [self dispatchGestureEvent:event];
1058 }
1059 
1060 - (void)rotateWithEvent:(NSEvent*)event {
1061  [self dispatchGestureEvent:event];
1062 }
1063 
1064 - (void)swipeWithEvent:(NSEvent*)event {
1065  // Not needed, it's handled by scrollWheel.
1066 }
1067 
1068 - (void)touchesBeganWithEvent:(NSEvent*)event {
1069  NSTouch* touch = event.allTouches.anyObject;
1070  if (touch != nil) {
1071  if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
1072  kTrackpadTouchInertiaCancelWindowMs) {
1073  // The trackpad has been touched following a scroll momentum event.
1074  // A scroll inertia cancel message should be sent to the framework.
1075  NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
1076  NSPoint locationInBackingCoordinates =
1077  [self.flutterView convertPointToBacking:locationInView];
1078  FlutterPointerEvent flutterEvent = {
1079  .struct_size = sizeof(flutterEvent),
1080  .timestamp = static_cast<size_t>(event.timestamp * USEC_PER_SEC),
1081  .x = locationInBackingCoordinates.x,
1082  .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative.
1083  .device = kPointerPanZoomDeviceId,
1084  .signal_kind = kFlutterPointerSignalKindScrollInertiaCancel,
1085  .device_kind = kFlutterPointerDeviceKindTrackpad,
1086  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
1087  };
1088 
1089  [_engine sendPointerEvent:flutterEvent];
1090  // Ensure no further scroll inertia cancel event will be sent.
1091  _mouseState.last_scroll_momentum_changed_time = 0;
1092  }
1093  }
1094 }
1095 
1096 @end
flutter::LayoutClue
Definition: FlutterKeyboardViewDelegate.h:20
FlutterEngine
Definition: FlutterEngine.h:31
FlutterViewController
Definition: FlutterViewController.h:73
FlutterEngine.h
FlutterViewWrapper
Definition: FlutterViewController.mm:154
FlutterEngine_Internal.h
+[FlutterDartProject lookupKeyForAsset:]
NSString * lookupKeyForAsset:(NSString *asset)
Definition: FlutterDartProject.mm:116
_keyboardLayoutNotifier
flutter::KeyboardLayoutNotifier _keyboardLayoutNotifier
Definition: FlutterKeyboardManagerTest.mm:243
_bridge
std::shared_ptr< flutter::AccessibilityBridgeMac > _bridge
Definition: FlutterViewController.mm:333
FlutterChannels.h
FlutterRenderer.h
_project
FlutterDartProject * _project
Definition: FlutterEngine.mm:409
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterPluginRegistrar-p
Definition: FlutterPluginRegistrarMacOS.h:28
FlutterKeyPrimaryResponder.h
-[FlutterView setBackgroundColor:]
void setBackgroundColor:(nonnull NSColor *color)
OnKeyboardLayoutChanged
static void OnKeyboardLayoutChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo)
Definition: FlutterViewController.mm:239
_controller
__weak FlutterViewController * _controller
Definition: FlutterViewController.mm:250
FlutterThreadSynchronizer
Definition: FlutterThreadSynchronizer.h:18
flutter::KeyboardLayoutNotifier
void(^ KeyboardLayoutNotifier)()
Definition: FlutterKeyboardViewDelegate.h:16
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:27
FlutterCodecs.h
FlutterKeyboardManager.h
FlutterViewController_Internal.h
_threadSynchronizer
FlutterThreadSynchronizer * _threadSynchronizer
Definition: FlutterViewController.mm:341
FlutterViewController::viewIdentifier
FlutterViewIdentifier viewIdentifier
Definition: FlutterViewController.h:130
FlutterView
Definition: FlutterView.h:35
FlutterTextInputSemanticsObject.h
+[FlutterDartProject lookupKeyForAsset:fromPackage:]
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
Definition: FlutterDartProject.mm:125
FlutterDartProject
Definition: FlutterDartProject.mm:24
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:27
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
FlutterView.h
FlutterViewIdentifier
int64_t FlutterViewIdentifier
Definition: FlutterViewController.h:21
FlutterViewDelegate-p
Definition: FlutterView.h:18
FlutterViewController.h