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