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  NSCAssert(controller.engine != nil,
331  @"The FlutterViewController unexpectedly stays unattached after initialization. "
332  @"In unit tests, this is likely because either the FlutterViewController or "
333  @"the FlutterEngine is mocked. Please subclass these classes instead.",
334  controller.engine, controller.viewIdentifier);
335  controller->_mouseTrackingMode = kFlutterMouseTrackingModeInKeyWindow;
336  [controller notifySemanticsEnabledChanged];
337 }
338 
339 - (instancetype)initWithCoder:(NSCoder*)coder {
340  self = [super initWithCoder:coder];
341  NSAssert(self, @"Super init cannot be nil");
342 
343  CommonInit(self, nil);
344  return self;
345 }
346 
347 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
348  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
349  NSAssert(self, @"Super init cannot be nil");
350 
351  CommonInit(self, nil);
352  return self;
353 }
354 
355 - (instancetype)initWithProject:(nullable FlutterDartProject*)project {
356  self = [super initWithNibName:nil bundle:nil];
357  NSAssert(self, @"Super init cannot be nil");
358 
359  _project = project;
360  CommonInit(self, nil);
361  return self;
362 }
363 
364 - (instancetype)initWithEngine:(nonnull FlutterEngine*)engine
365  nibName:(nullable NSString*)nibName
366  bundle:(nullable NSBundle*)nibBundle {
367  NSAssert(engine != nil, @"Engine is required");
368 
369  self = [super initWithNibName:nibName bundle:nibBundle];
370  if (self) {
371  CommonInit(self, engine);
372  }
373 
374  return self;
375 }
376 
377 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
378  return [_engine.keyboardManager isDispatchingKeyEvent:event];
379 }
380 
381 - (void)loadView {
382  FlutterView* flutterView;
383  id<MTLDevice> device = _engine.renderer.device;
384  id<MTLCommandQueue> commandQueue = _engine.renderer.commandQueue;
385  if (!device || !commandQueue) {
386  NSLog(@"Unable to create FlutterView; no MTLDevice or MTLCommandQueue available.");
387  return;
388  }
389  flutterView = [self createFlutterViewWithMTLDevice:device commandQueue:commandQueue];
390  if (_backgroundColor != nil) {
391  [flutterView setBackgroundColor:_backgroundColor];
392  }
393  FlutterViewWrapper* wrapperView = [[FlutterViewWrapper alloc] initWithFlutterView:flutterView
394  controller:self];
395  self.view = wrapperView;
396  _flutterView = flutterView;
397 }
398 
399 - (void)viewDidLoad {
400  [self configureTrackingArea];
401  [self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
402  [self.view setWantsRestingTouches:YES];
403  [_engine viewControllerViewDidLoad:self];
404 }
405 
406 - (void)viewWillAppear {
407  [super viewWillAppear];
408  if (!_engine.running) {
409  [self launchEngine];
410  }
411  [self listenForMetaModifiedKeyUpEvents];
412 }
413 
414 - (void)viewWillDisappear {
415  // Per Apple's documentation, it is discouraged to call removeMonitor: in dealloc, and it's
416  // recommended to be called earlier in the lifecycle.
417  [NSEvent removeMonitor:_keyUpMonitor];
418  _keyUpMonitor = nil;
419 }
420 
421 - (void)dealloc {
422  if ([self attached]) {
423  [_engine removeViewController:self];
424  }
425  [self.flutterView shutDown];
426 }
427 
428 #pragma mark - Public methods
429 
430 - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
431  if (_mouseTrackingMode == mode) {
432  return;
433  }
434  _mouseTrackingMode = mode;
435  [self configureTrackingArea];
436 }
437 
438 - (void)setBackgroundColor:(NSColor*)color {
439  _backgroundColor = color;
440  [_flutterView setBackgroundColor:_backgroundColor];
441 }
442 
443 - (FlutterViewIdentifier)viewIdentifier {
444  NSAssert([self attached], @"This view controller is not attached.");
445  return _viewIdentifier;
446 }
447 
448 - (void)onPreEngineRestart {
449 }
450 
451 - (void)notifySemanticsEnabledChanged {
452  BOOL mySemanticsEnabled = !!_bridge;
453  BOOL newSemanticsEnabled = _engine.semanticsEnabled;
454  if (newSemanticsEnabled == mySemanticsEnabled) {
455  return;
456  }
457  if (newSemanticsEnabled) {
458  _bridge = [self createAccessibilityBridgeWithEngine:_engine];
459  } else {
460  // Remove the accessibility children from flutter view before resetting the bridge.
461  _flutterView.accessibilityChildren = nil;
462  _bridge.reset();
463  }
464  NSAssert(newSemanticsEnabled == !!_bridge, @"Failed to update semantics for the view.");
465 }
466 
467 - (std::weak_ptr<flutter::AccessibilityBridgeMac>)accessibilityBridge {
468  return _bridge;
469 }
470 
471 - (void)setUpWithEngine:(FlutterEngine*)engine
472  viewIdentifier:(FlutterViewIdentifier)viewIdentifier {
473  NSAssert(_engine == nil, @"Already attached to an engine %@.", _engine);
474  _engine = engine;
475  _viewIdentifier = viewIdentifier;
476 }
477 
478 - (void)detachFromEngine {
479  NSAssert(_engine != nil, @"Not attached to any engine.");
480  _engine = nil;
481 }
482 
483 - (BOOL)attached {
484  return _engine != nil;
485 }
486 
487 - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
488  // Semantics will be disabled when unfocusing application but the updateSemantics:
489  // callback is received in next run loop turn.
490  if (!_engine.semanticsEnabled) {
491  return;
492  }
493  for (size_t i = 0; i < update->node_count; i++) {
494  const FlutterSemanticsNode2* node = update->nodes[i];
495  _bridge->AddFlutterSemanticsNodeUpdate(*node);
496  }
497 
498  for (size_t i = 0; i < update->custom_action_count; i++) {
499  const FlutterSemanticsCustomAction2* action = update->custom_actions[i];
500  _bridge->AddFlutterSemanticsCustomActionUpdate(*action);
501  }
502 
503  _bridge->CommitUpdates();
504 
505  // Accessibility tree can only be used when the view is loaded.
506  if (!self.viewLoaded) {
507  return;
508  }
509  // Attaches the accessibility root to the flutter view.
510  auto root = _bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
511  if (root) {
512  if ([self.flutterView.accessibilityChildren count] == 0) {
513  NSAccessibilityElement* native_root = root->GetNativeViewAccessible();
514  self.flutterView.accessibilityChildren = @[ native_root ];
515  }
516  } else {
517  self.flutterView.accessibilityChildren = nil;
518  }
519 }
520 
521 #pragma mark - Private methods
522 
523 - (BOOL)launchEngine {
524  if (![_engine runWithEntrypoint:nil]) {
525  return NO;
526  }
527  return YES;
528 }
529 
530 // macOS does not call keyUp: on a key while the command key is pressed. This results in a loss
531 // of a key event once the modified key is released. This method registers the
532 // ViewController as a listener for a keyUp event before it's handled by NSApplication, and should
533 // NOT modify the event to avoid any unexpected behavior.
534 - (void)listenForMetaModifiedKeyUpEvents {
535  if (_keyUpMonitor != nil) {
536  // It is possible for [NSViewController viewWillAppear] to be invoked multiple times
537  // in a row. https://github.com/flutter/flutter/issues/105963
538  return;
539  }
540  FlutterViewController* __weak weakSelf = self;
541  _keyUpMonitor = [NSEvent
542  addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
543  handler:^NSEvent*(NSEvent* event) {
544  // Intercept keyUp only for events triggered on the current
545  // view or textInputPlugin.
546  NSResponder* firstResponder = [[event window] firstResponder];
547  if (weakSelf.viewLoaded && weakSelf.flutterView &&
548  (firstResponder == weakSelf.flutterView ||
549  firstResponder == weakSelf.activeTextInputPlugin) &&
550  ([event modifierFlags] & NSEventModifierFlagCommand) &&
551  ([event type] == NSEventTypeKeyUp)) {
552  [weakSelf keyUp:event];
553  }
554  return event;
555  }];
556 }
557 
558 - (void)configureTrackingArea {
559  if (!self.viewLoaded) {
560  // The viewDidLoad will call configureTrackingArea again when
561  // the view is actually loaded.
562  return;
563  }
564  if (_mouseTrackingMode != kFlutterMouseTrackingModeNone && self.flutterView) {
565  NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
566  NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag;
567  switch (_mouseTrackingMode) {
568  case kFlutterMouseTrackingModeInKeyWindow:
569  options |= NSTrackingActiveInKeyWindow;
570  break;
571  case kFlutterMouseTrackingModeInActiveApp:
572  options |= NSTrackingActiveInActiveApp;
573  break;
574  case kFlutterMouseTrackingModeAlways:
575  options |= NSTrackingActiveAlways;
576  break;
577  default:
578  NSLog(@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
579  return;
580  }
581  _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
582  options:options
583  owner:self
584  userInfo:nil];
585  [self.flutterView addTrackingArea:_trackingArea];
586  } else if (_trackingArea) {
587  [self.flutterView removeTrackingArea:_trackingArea];
588  _trackingArea = nil;
589  }
590 }
591 
592 - (void)dispatchMouseEvent:(nonnull NSEvent*)event {
593  FlutterPointerPhase phase = _mouseState.buttons == 0
594  ? (_mouseState.flutter_state_is_down ? kUp : kHover)
595  : (_mouseState.flutter_state_is_down ? kMove : kDown);
596  [self dispatchMouseEvent:event phase:phase];
597 }
598 
599 - (void)dispatchGestureEvent:(nonnull NSEvent*)event {
600  if (event.phase == NSEventPhaseBegan || event.phase == NSEventPhaseMayBegin) {
601  [self dispatchMouseEvent:event phase:kPanZoomStart];
602  } else if (event.phase == NSEventPhaseChanged) {
603  [self dispatchMouseEvent:event phase:kPanZoomUpdate];
604  } else if (event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled) {
605  [self dispatchMouseEvent:event phase:kPanZoomEnd];
606  } else if (event.phase == NSEventPhaseNone && event.momentumPhase == NSEventPhaseNone) {
607  [self dispatchMouseEvent:event phase:kHover];
608  } else {
609  // Waiting until the first momentum change event is a workaround for an issue where
610  // touchesBegan: is called unexpectedly while in low power mode within the interval between
611  // momentum start and the first momentum change.
612  if (event.momentumPhase == NSEventPhaseChanged) {
613  _mouseState.last_scroll_momentum_changed_time = event.timestamp;
614  }
615  // Skip momentum update events, the framework will generate scroll momentum.
616  NSAssert(event.momentumPhase != NSEventPhaseNone,
617  @"Received gesture event with unexpected phase");
618  }
619 }
620 
621 - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
622  NSAssert(self.viewLoaded, @"View must be loaded before it handles the mouse event");
623  // There are edge cases where the system will deliver enter out of order relative to other
624  // events (e.g., drag out and back in, release, then click; mouseDown: will be called before
625  // mouseEntered:). Discard those events, since the add will already have been synthesized.
626  if (_mouseState.flutter_state_is_added && phase == kAdd) {
627  return;
628  }
629 
630  // Multiple gesture recognizers could be active at once, we can't send multiple kPanZoomStart.
631  // For example: rotation and magnification.
632  if (phase == kPanZoomStart || phase == kPanZoomEnd) {
633  if (event.type == NSEventTypeScrollWheel) {
634  _mouseState.pan_gesture_phase = event.phase;
635  } else if (event.type == NSEventTypeMagnify) {
636  _mouseState.scale_gesture_phase = event.phase;
637  } else if (event.type == NSEventTypeRotate) {
638  _mouseState.rotate_gesture_phase = event.phase;
639  }
640  }
641  if (phase == kPanZoomStart) {
642  if (event.type == NSEventTypeScrollWheel) {
643  // Ensure scroll inertia cancel event is not sent afterwards.
644  _mouseState.last_scroll_momentum_changed_time = 0;
645  }
646  if (_mouseState.flutter_state_is_pan_zoom_started) {
647  // Already started on a previous gesture type
648  return;
649  }
650  _mouseState.flutter_state_is_pan_zoom_started = true;
651  }
652  if (phase == kPanZoomEnd) {
653  if (!_mouseState.flutter_state_is_pan_zoom_started) {
654  // NSEventPhaseCancelled is sometimes received at incorrect times in the state
655  // machine, just ignore it here if it doesn't make sense
656  // (we have no active gesture to cancel).
657  NSAssert(event.phase == NSEventPhaseCancelled,
658  @"Received gesture event with unexpected phase");
659  return;
660  }
661  // NSEventPhase values are powers of two, we can use this to inspect merged phases.
662  NSEventPhase all_gestures_fields = _mouseState.pan_gesture_phase |
663  _mouseState.scale_gesture_phase |
664  _mouseState.rotate_gesture_phase;
665  NSEventPhase active_mask = NSEventPhaseBegan | NSEventPhaseChanged;
666  if ((all_gestures_fields & active_mask) != 0) {
667  // Even though this gesture type ended, a different type is still active.
668  return;
669  }
670  }
671 
672  // If a pointer added event hasn't been sent, synthesize one using this event for the basic
673  // information.
674  if (!_mouseState.flutter_state_is_added && phase != kAdd) {
675  // Only the values extracted for use in flutterEvent below matter, the rest are dummy values.
676  NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
677  location:event.locationInWindow
678  modifierFlags:0
679  timestamp:event.timestamp
680  windowNumber:event.windowNumber
681  context:nil
682  eventNumber:0
683  trackingNumber:0
684  userData:NULL];
685  [self dispatchMouseEvent:addEvent phase:kAdd];
686  }
687 
688  NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
689  NSPoint locationInBackingCoordinates = [self.flutterView convertPointToBacking:locationInView];
690  int32_t device = kMousePointerDeviceId;
691  FlutterPointerDeviceKind deviceKind = kFlutterPointerDeviceKindMouse;
692  if (phase == kPanZoomStart || phase == kPanZoomUpdate || phase == kPanZoomEnd) {
693  device = kPointerPanZoomDeviceId;
694  deviceKind = kFlutterPointerDeviceKindTrackpad;
695  }
696  FlutterPointerEvent flutterEvent = {
697  .struct_size = sizeof(flutterEvent),
698  .phase = phase,
699  .timestamp = static_cast<size_t>(event.timestamp * USEC_PER_SEC),
700  .x = locationInBackingCoordinates.x,
701  .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative.
702  .device = device,
703  .device_kind = deviceKind,
704  // If a click triggered a synthesized kAdd, don't pass the buttons in that event.
705  .buttons = phase == kAdd ? 0 : _mouseState.buttons,
706  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
707  };
708 
709  if (phase == kPanZoomUpdate) {
710  if (event.type == NSEventTypeScrollWheel) {
711  _mouseState.delta_x += event.scrollingDeltaX * self.flutterView.layer.contentsScale;
712  _mouseState.delta_y += event.scrollingDeltaY * self.flutterView.layer.contentsScale;
713  } else if (event.type == NSEventTypeMagnify) {
714  _mouseState.scale += event.magnification;
715  } else if (event.type == NSEventTypeRotate) {
716  _mouseState.rotation += event.rotation * (-M_PI / 180.0);
717  }
718  flutterEvent.pan_x = _mouseState.delta_x;
719  flutterEvent.pan_y = _mouseState.delta_y;
720  // Scale value needs to be normalized to range 0->infinity.
721  flutterEvent.scale = pow(2.0, _mouseState.scale);
722  flutterEvent.rotation = _mouseState.rotation;
723  } else if (phase == kPanZoomEnd) {
724  _mouseState.GestureReset();
725  } else if (phase != kPanZoomStart && event.type == NSEventTypeScrollWheel) {
726  flutterEvent.signal_kind = kFlutterPointerSignalKindScroll;
727 
728  double pixelsPerLine = 1.0;
729  if (!event.hasPreciseScrollingDeltas) {
730  // The scrollingDelta needs to be multiplied by the line height.
731  // CGEventSourceGetPixelsPerLine() will return 10, which will result in
732  // scrolling that is noticeably slower than in other applications.
733  // Using 40.0 as the multiplier to match Chromium.
734  // See https://source.chromium.org/chromium/chromium/src/+/main:ui/events/cocoa/events_mac.mm
735  pixelsPerLine = 40.0;
736  }
737  double scaleFactor = self.flutterView.layer.contentsScale;
738  // When mouse input is received while shift is pressed (regardless of
739  // any other pressed keys), Mac automatically flips the axis. Other
740  // platforms do not do this, so we flip it back to normalize the input
741  // received by the framework. The keyboard+mouse-scroll mechanism is exposed
742  // in the ScrollBehavior of the framework so developers can customize the
743  // behavior.
744  // At time of change, Apple does not expose any other type of API or signal
745  // that the X/Y axes have been flipped.
746  double scaledDeltaX = -event.scrollingDeltaX * pixelsPerLine * scaleFactor;
747  double scaledDeltaY = -event.scrollingDeltaY * pixelsPerLine * scaleFactor;
748  if (event.modifierFlags & NSShiftKeyMask) {
749  flutterEvent.scroll_delta_x = scaledDeltaY;
750  flutterEvent.scroll_delta_y = scaledDeltaX;
751  } else {
752  flutterEvent.scroll_delta_x = scaledDeltaX;
753  flutterEvent.scroll_delta_y = scaledDeltaY;
754  }
755  }
756 
757  [_engine.keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
758  [_engine sendPointerEvent:flutterEvent];
759 
760  // Update tracking of state as reported to Flutter.
761  if (phase == kDown) {
762  _mouseState.flutter_state_is_down = true;
763  } else if (phase == kUp) {
764  _mouseState.flutter_state_is_down = false;
765  if (_mouseState.has_pending_exit) {
766  [self dispatchMouseEvent:event phase:kRemove];
767  _mouseState.has_pending_exit = false;
768  }
769  } else if (phase == kAdd) {
770  _mouseState.flutter_state_is_added = true;
771  } else if (phase == kRemove) {
772  _mouseState.Reset();
773  }
774 }
775 
776 - (void)onAccessibilityStatusChanged:(BOOL)enabled {
777  if (!enabled && self.viewLoaded && [self.activeTextInputPlugin isFirstResponder]) {
778  // Normally TextInputPlugin, when editing, is child of FlutterViewWrapper.
779  // When accessibility is enabled the TextInputPlugin gets added as an indirect
780  // child to FlutterTextField. When disabling the plugin needs to be reparented
781  // back.
782  [self.view addSubview:self.activeTextInputPlugin];
783  }
784 }
785 
786 - (std::shared_ptr<flutter::AccessibilityBridgeMac>)createAccessibilityBridgeWithEngine:
787  (nonnull FlutterEngine*)engine {
788  return std::make_shared<flutter::AccessibilityBridgeMac>(engine, self);
789 }
790 
791 - (nonnull FlutterView*)createFlutterViewWithMTLDevice:(id<MTLDevice>)device
792  commandQueue:(id<MTLCommandQueue>)commandQueue {
793  return [[FlutterView alloc] initWithMTLDevice:device
794  commandQueue:commandQueue
795  delegate:self
796  viewIdentifier:_viewIdentifier];
797 }
798 
799 - (NSString*)lookupKeyForAsset:(NSString*)asset {
800  return [FlutterDartProject lookupKeyForAsset:asset];
801 }
802 
803 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
804  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
805 }
806 
807 #pragma mark - FlutterViewDelegate
808 
809 /**
810  * Responds to view reshape by notifying the engine of the change in dimensions.
811  */
812 - (void)viewDidReshape:(NSView*)view {
813  FML_DCHECK(view == _flutterView);
814  [_engine updateWindowMetricsForViewController:self];
815 }
816 
817 - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view {
818  FML_DCHECK(view == _flutterView);
819  // Only allow FlutterView to become first responder if TextInputPlugin is
820  // not active. Otherwise a mouse event inside FlutterView would cause the
821  // TextInputPlugin to lose first responder status.
822  return !self.activeTextInputPlugin.isFirstResponder;
823 }
824 
825 #pragma mark - FlutterPluginRegistry
826 
827 - (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName {
828  return [_engine registrarForPlugin:pluginName];
829 }
830 
831 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
832  return [_engine valuePublishedByPlugin:pluginKey];
833 }
834 
835 #pragma mark - FlutterKeyboardViewDelegate
836 
837 - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
838  return [self.activeTextInputPlugin handleKeyEvent:event];
839 }
840 
841 #pragma mark - NSResponder
842 
843 - (BOOL)acceptsFirstResponder {
844  return YES;
845 }
846 
847 - (void)keyDown:(NSEvent*)event {
848  [_engine.keyboardManager handleEvent:event withContext:self];
849 }
850 
851 - (void)keyUp:(NSEvent*)event {
852  [_engine.keyboardManager handleEvent:event withContext:self];
853 }
854 
855 - (void)flagsChanged:(NSEvent*)event {
856  [_engine.keyboardManager handleEvent:event withContext:self];
857 }
858 
859 - (void)mouseEntered:(NSEvent*)event {
860  if (_mouseState.has_pending_exit) {
861  _mouseState.has_pending_exit = false;
862  } else {
863  [self dispatchMouseEvent:event phase:kAdd];
864  }
865 }
866 
867 - (void)mouseExited:(NSEvent*)event {
868  if (_mouseState.buttons != 0) {
869  _mouseState.has_pending_exit = true;
870  return;
871  }
872  [self dispatchMouseEvent:event phase:kRemove];
873 }
874 
875 - (void)mouseDown:(NSEvent*)event {
876  _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
877  [self dispatchMouseEvent:event];
878 }
879 
880 - (void)mouseUp:(NSEvent*)event {
881  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
882  [self dispatchMouseEvent:event];
883 }
884 
885 - (void)mouseDragged:(NSEvent*)event {
886  [self dispatchMouseEvent:event];
887 }
888 
889 - (void)rightMouseDown:(NSEvent*)event {
890  _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
891  [self dispatchMouseEvent:event];
892 }
893 
894 - (void)rightMouseUp:(NSEvent*)event {
895  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
896  [self dispatchMouseEvent:event];
897 }
898 
899 - (void)rightMouseDragged:(NSEvent*)event {
900  [self dispatchMouseEvent:event];
901 }
902 
903 - (void)otherMouseDown:(NSEvent*)event {
904  _mouseState.buttons |= (1 << event.buttonNumber);
905  [self dispatchMouseEvent:event];
906 }
907 
908 - (void)otherMouseUp:(NSEvent*)event {
909  _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
910  [self dispatchMouseEvent:event];
911 }
912 
913 - (void)otherMouseDragged:(NSEvent*)event {
914  [self dispatchMouseEvent:event];
915 }
916 
917 - (void)mouseMoved:(NSEvent*)event {
918  [self dispatchMouseEvent:event];
919 }
920 
921 - (void)scrollWheel:(NSEvent*)event {
922  [self dispatchGestureEvent:event];
923 }
924 
925 - (void)magnifyWithEvent:(NSEvent*)event {
926  [self dispatchGestureEvent:event];
927 }
928 
929 - (void)rotateWithEvent:(NSEvent*)event {
930  [self dispatchGestureEvent:event];
931 }
932 
933 - (void)swipeWithEvent:(NSEvent*)event {
934  // Not needed, it's handled by scrollWheel.
935 }
936 
937 - (void)touchesBeganWithEvent:(NSEvent*)event {
938  NSTouch* touch = event.allTouches.anyObject;
939  if (touch != nil) {
940  if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
941  kTrackpadTouchInertiaCancelWindowMs) {
942  // The trackpad has been touched following a scroll momentum event.
943  // A scroll inertia cancel message should be sent to the framework.
944  NSPoint locationInView = [self.flutterView convertPoint:event.locationInWindow fromView:nil];
945  NSPoint locationInBackingCoordinates =
946  [self.flutterView convertPointToBacking:locationInView];
947  FlutterPointerEvent flutterEvent = {
948  .struct_size = sizeof(flutterEvent),
949  .timestamp = static_cast<size_t>(event.timestamp * USEC_PER_SEC),
950  .x = locationInBackingCoordinates.x,
951  .y = -locationInBackingCoordinates.y, // convertPointToBacking makes this negative.
952  .device = kPointerPanZoomDeviceId,
953  .signal_kind = kFlutterPointerSignalKindScrollInertiaCancel,
954  .device_kind = kFlutterPointerDeviceKindTrackpad,
955  .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
956  };
957 
958  [_engine sendPointerEvent:flutterEvent];
959  // Ensure no further scroll inertia cancel event will be sent.
960  _mouseState.last_scroll_momentum_changed_time = 0;
961  }
962  }
963 }
964 
965 @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)