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