| 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
5 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterViewController.h"
6 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterViewController_Internal.h"
7
8 #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"
13 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
14 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
15 #import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
16 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
17 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyPrimaryResponder.h"
18 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterKeyboardManager.h"
19 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterRenderer.h"
20 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputSemanticsObject.h"
21 #import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.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 */
161 @interface FlutterViewController () <FlutterViewDelegate, FlutterKeyboardManagerEventContext>
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;
212 __weak FlutterViewController* _controller;
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.
295 FlutterDartProject* _project;
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
FlutterChannels.h
FlutterCodecs.h
FlutterEngine.h
_project
FlutterDartProject * _project
Definition: FlutterEngine.mm:415
FlutterEngine_Internal.h
FlutterKeyPrimaryResponder.h
FlutterKeyboardManager.h
FlutterRenderer.h
FlutterTextInputSemanticsObject.h
FlutterView.h
FlutterViewController.h
FlutterViewIdentifier
int64_t FlutterViewIdentifier
Definition: FlutterViewController.h:21
_controller
__weak FlutterViewController * _controller
Definition: FlutterViewController.mm:210
_bridge
std::shared_ptr< flutter::AccessibilityBridgeMac > _bridge
Definition: FlutterViewController.mm:293
FlutterViewController_Internal.h
FlutterDartProject
Definition: FlutterDartProject.mm:24
+[FlutterDartProject lookupKeyForAsset:fromPackage:]
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
Definition: FlutterDartProject.mm:134
+[FlutterDartProject lookupKeyForAsset:]
NSString * lookupKeyForAsset:(NSString *asset)
Definition: FlutterDartProject.mm:125
FlutterEngine
Definition: FlutterEngine.h:32
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:42
FlutterViewController
Definition: FlutterViewController.h:73
FlutterViewController::engine
FlutterEngine * engine
Definition: FlutterViewController.h:78
FlutterViewController::viewIdentifier
FlutterViewIdentifier viewIdentifier
Definition: FlutterViewController.h:130
FlutterView
Definition: FlutterView.h:36
-[FlutterView setBackgroundColor:]
void setBackgroundColor:(nonnull NSColor *color)
FlutterViewWrapper
Definition: FlutterViewController.mm:153
FlutterKeyboardManagerEventContext-p
Definition: FlutterKeyboardManager.h:40
FlutterPluginRegistrar-p
Definition: FlutterPluginRegistrarMacOS.h:28
FlutterViewDelegate-p
Definition: FlutterView.h:18