8 #include <Carbon/Carbon.h>
9 #import <objc/message.h>
11 #include "flutter/common/constants.h"
12 #include "flutter/fml/platform/darwin/cf_utils.h"
22 #include "flutter/shell/platform/embedder/embedder.h"
24 #pragma mark - Static types and data.
30 static constexpr int32_t kMousePointerDeviceId = 0;
31 static constexpr int32_t kPointerPanZoomDeviceId = 1;
36 static constexpr
double kTrackpadTouchInertiaCancelWindowMs = 0.050;
69 bool flutter_state_is_added =
false;
74 bool flutter_state_is_down =
false;
83 bool has_pending_exit =
false;
88 bool flutter_state_is_pan_zoom_started =
false;
93 NSEventPhase pan_gesture_phase = NSEventPhaseNone;
98 NSEventPhase scale_gesture_phase = NSEventPhaseNone;
103 NSEventPhase rotate_gesture_phase = NSEventPhaseNone;
108 NSTimeInterval last_scroll_momentum_changed_time = 0;
113 void GestureReset() {
118 flutter_state_is_pan_zoom_started =
false;
119 pan_gesture_phase = NSEventPhaseNone;
120 scale_gesture_phase = NSEventPhaseNone;
121 rotate_gesture_phase = NSEventPhaseNone;
128 flutter_state_is_added =
false;
129 flutter_state_is_down =
false;
130 has_pending_exit =
false;
137 #pragma mark - Private interface declaration.
154 - (void)setBackgroundColor:(NSColor*)color;
166 @property(nonatomic) NSTrackingArea* trackingArea;
171 @property(nonatomic) MouseState mouseState;
176 @property(nonatomic)
id keyUpMonitor;
181 - (BOOL)launchEngine;
187 - (void)configureTrackingArea;
194 - (void)dispatchMouseEvent:(nonnull NSEvent*)event;
199 - (void)dispatchGestureEvent:(nonnull NSEvent*)event;
204 - (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;
208 #pragma mark - FlutterViewWrapper implementation.
215 - (instancetype)initWithFlutterView:(
FlutterView*)view
217 self = [
super initWithFrame:NSZeroRect];
221 view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
222 [
self addSubview:view];
227 - (void)setBackgroundColor:(NSColor*)color {
228 [_flutterView setBackgroundColor:color];
231 - (BOOL)performKeyEquivalent:(NSEvent*)event {
238 if (
self.window.firstResponder != _flutterView || [
_controller isDispatchingKeyEvent:event]) {
239 return [
super performKeyEquivalent:event];
241 [_flutterView keyDown:event];
245 - (NSArray*)accessibilityChildren {
246 return @[ _flutterView ];
251 - (void)mouseDown:(NSEvent*)event {
252 if (@available(macOS 13.3.1, *)) {
253 [
super mouseDown:event];
265 [
self.nextResponder mouseDown:event];
271 - (void)mouseUp:(NSEvent*)event {
272 if (@available(macOS 13.3.1, *)) {
273 [
super mouseUp:event];
285 [
self.nextResponder mouseUp:event];
291 #pragma mark - FlutterViewController implementation.
297 std::shared_ptr<flutter::AccessibilityBridgeMac>
_bridge;
301 @synthesize viewIdentifier = _viewIdentifier;
303 @dynamic accessibilityBridge;
309 if (_engine.textInputPlugin.currentViewController ==
self) {
310 return _engine.textInputPlugin;
322 project:controller->_project
323 allowHeadlessExecution:NO];
325 NSCAssert(controller.
engine == nil,
326 @"The FlutterViewController is unexpectedly attached to "
327 @"engine %@ before initialization.",
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.",
335 controller->_mouseTrackingMode = kFlutterMouseTrackingModeInKeyWindow;
336 [controller notifySemanticsEnabledChanged];
339 - (instancetype)initWithCoder:(NSCoder*)coder {
340 self = [
super initWithCoder:coder];
341 NSAssert(
self,
@"Super init cannot be nil");
343 CommonInit(
self, nil);
347 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
348 self = [
super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
349 NSAssert(
self,
@"Super init cannot be nil");
351 CommonInit(
self, nil);
356 self = [
super initWithNibName:nil bundle:nil];
357 NSAssert(
self,
@"Super init cannot be nil");
360 CommonInit(
self, nil);
364 - (instancetype)initWithEngine:(nonnull
FlutterEngine*)engine
365 nibName:(nullable NSString*)nibName
366 bundle:(nullable NSBundle*)nibBundle {
367 NSAssert(engine != nil,
@"Engine is required");
369 self = [
super initWithNibName:nibName bundle:nibBundle];
371 CommonInit(
self, engine);
377 - (BOOL)isDispatchingKeyEvent:(NSEvent*)event {
378 return [_engine.keyboardManager isDispatchingKeyEvent:event];
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.");
389 flutterView = [
self createFlutterViewWithMTLDevice:device commandQueue:commandQueue];
390 if (_backgroundColor != nil) {
395 self.view = wrapperView;
396 _flutterView = flutterView;
399 - (void)viewDidLoad {
400 [
self configureTrackingArea];
401 [
self.view setAllowedTouchTypes:NSTouchTypeMaskIndirect];
402 [
self.view setWantsRestingTouches:YES];
403 [_engine viewControllerViewDidLoad:self];
406 - (void)viewWillAppear {
407 [
super viewWillAppear];
408 if (!_engine.running) {
411 [
self listenForMetaModifiedKeyUpEvents];
414 - (void)viewWillDisappear {
417 [NSEvent removeMonitor:_keyUpMonitor];
422 if ([
self attached]) {
423 [_engine removeViewController:self];
425 [
self.flutterView shutDown];
428 #pragma mark - Public methods
430 - (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
431 if (_mouseTrackingMode == mode) {
434 _mouseTrackingMode = mode;
435 [
self configureTrackingArea];
438 - (void)setBackgroundColor:(NSColor*)color {
439 _backgroundColor = color;
440 [_flutterView setBackgroundColor:_backgroundColor];
444 NSAssert([
self attached],
@"This view controller is not attached.");
445 return _viewIdentifier;
448 - (void)onPreEngineRestart {
451 - (void)notifySemanticsEnabledChanged {
452 BOOL mySemanticsEnabled = !!
_bridge;
453 BOOL newSemanticsEnabled = _engine.semanticsEnabled;
454 if (newSemanticsEnabled == mySemanticsEnabled) {
457 if (newSemanticsEnabled) {
458 _bridge = [
self createAccessibilityBridgeWithEngine:_engine];
461 _flutterView.accessibilityChildren = nil;
464 NSAssert(newSemanticsEnabled == !!
_bridge,
@"Failed to update semantics for the view.");
467 - (std::weak_ptr<flutter::AccessibilityBridgeMac>)accessibilityBridge {
473 NSAssert(_engine == nil,
@"Already attached to an engine %@.", _engine);
475 _viewIdentifier = viewIdentifier;
478 - (void)detachFromEngine {
479 NSAssert(_engine != nil,
@"Not attached to any engine.");
484 return _engine != nil;
487 - (void)updateSemantics:(const FlutterSemanticsUpdate2*)update {
490 if (!_engine.semanticsEnabled) {
493 for (
size_t i = 0; i < update->node_count; i++) {
494 const FlutterSemanticsNode2* node = update->nodes[i];
495 _bridge->AddFlutterSemanticsNodeUpdate(*node);
498 for (
size_t i = 0; i < update->custom_action_count; i++) {
499 const FlutterSemanticsCustomAction2* action = update->custom_actions[i];
500 _bridge->AddFlutterSemanticsCustomActionUpdate(*action);
506 if (!
self.viewLoaded) {
510 auto root =
_bridge->GetFlutterPlatformNodeDelegateFromID(0).lock();
512 if ([
self.flutterView.accessibilityChildren count] == 0) {
513 NSAccessibilityElement* native_root = root->GetNativeViewAccessible();
514 self.flutterView.accessibilityChildren = @[ native_root ];
517 self.flutterView.accessibilityChildren = nil;
521 #pragma mark - Private methods
523 - (BOOL)launchEngine {
524 if (![_engine runWithEntrypoint:nil]) {
534 - (void)listenForMetaModifiedKeyUpEvents {
535 if (_keyUpMonitor != nil) {
541 _keyUpMonitor = [NSEvent
542 addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp
543 handler:^NSEvent*(NSEvent* event) {
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];
558 - (void)configureTrackingArea {
559 if (!
self.viewLoaded) {
564 if (_mouseTrackingMode != kFlutterMouseTrackingModeNone &&
self.flutterView) {
565 NSTrackingAreaOptions options = NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
566 NSTrackingInVisibleRect | NSTrackingEnabledDuringMouseDrag;
567 switch (_mouseTrackingMode) {
568 case kFlutterMouseTrackingModeInKeyWindow:
569 options |= NSTrackingActiveInKeyWindow;
571 case kFlutterMouseTrackingModeInActiveApp:
572 options |= NSTrackingActiveInActiveApp;
574 case kFlutterMouseTrackingModeAlways:
575 options |= NSTrackingActiveAlways;
578 NSLog(
@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
581 _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
585 [
self.flutterView addTrackingArea:_trackingArea];
586 }
else if (_trackingArea) {
587 [
self.flutterView removeTrackingArea:_trackingArea];
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];
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];
612 if (event.momentumPhase == NSEventPhaseChanged) {
613 _mouseState.last_scroll_momentum_changed_time =
event.timestamp;
616 NSAssert(event.momentumPhase != NSEventPhaseNone,
617 @"Received gesture event with unexpected phase");
621 - (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
622 NSAssert(
self.viewLoaded,
@"View must be loaded before it handles the mouse event");
626 if (_mouseState.flutter_state_is_added && phase == kAdd) {
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;
641 if (phase == kPanZoomStart) {
642 if (event.type == NSEventTypeScrollWheel) {
644 _mouseState.last_scroll_momentum_changed_time = 0;
646 if (_mouseState.flutter_state_is_pan_zoom_started) {
650 _mouseState.flutter_state_is_pan_zoom_started =
true;
652 if (phase == kPanZoomEnd) {
653 if (!_mouseState.flutter_state_is_pan_zoom_started) {
657 NSAssert(event.phase == NSEventPhaseCancelled,
658 @"Received gesture event with unexpected phase");
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) {
674 if (!_mouseState.flutter_state_is_added && phase != kAdd) {
676 NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
677 location:event.locationInWindow
679 timestamp:event.timestamp
680 windowNumber:event.windowNumber
685 [
self dispatchMouseEvent:addEvent phase:kAdd];
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;
696 FlutterPointerEvent flutterEvent = {
697 .struct_size =
sizeof(flutterEvent),
699 .timestamp =
static_cast<size_t>(event.timestamp * USEC_PER_SEC),
700 .x = locationInBackingCoordinates.x,
701 .y = -locationInBackingCoordinates.y,
703 .device_kind = deviceKind,
705 .buttons = phase == kAdd ? 0 : _mouseState.buttons,
706 .view_id = static_cast<FlutterViewIdentifier>(_viewIdentifier),
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);
718 flutterEvent.pan_x = _mouseState.delta_x;
719 flutterEvent.pan_y = _mouseState.delta_y;
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;
728 double pixelsPerLine = 1.0;
729 if (!event.hasPreciseScrollingDeltas) {
735 pixelsPerLine = 40.0;
737 double scaleFactor =
self.flutterView.layer.contentsScale;
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;
752 flutterEvent.scroll_delta_x = scaledDeltaX;
753 flutterEvent.scroll_delta_y = scaledDeltaY;
757 [_engine.keyboardManager syncModifiersIfNeeded:event.modifierFlags timestamp:event.timestamp];
758 [_engine sendPointerEvent:flutterEvent];
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;
769 }
else if (phase == kAdd) {
770 _mouseState.flutter_state_is_added =
true;
771 }
else if (phase == kRemove) {
776 - (void)onAccessibilityStatusChanged:(BOOL)enabled {
777 if (!enabled &&
self.viewLoaded && [
self.activeTextInputPlugin isFirstResponder]) {
782 [
self.view addSubview:self.activeTextInputPlugin];
786 - (std::shared_ptr<flutter::AccessibilityBridgeMac>)createAccessibilityBridgeWithEngine:
788 return std::make_shared<flutter::AccessibilityBridgeMac>(engine,
self);
791 - (nonnull
FlutterView*)createFlutterViewWithMTLDevice:(
id<MTLDevice>)device
792 commandQueue:(
id<MTLCommandQueue>)commandQueue {
793 return [[
FlutterView alloc] initWithMTLDevice:device
794 commandQueue:commandQueue
796 viewIdentifier:_viewIdentifier];
799 - (NSString*)lookupKeyForAsset:(NSString*)asset {
803 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
807 #pragma mark - FlutterViewDelegate
812 - (void)viewDidReshape:(NSView*)view {
813 FML_DCHECK(view == _flutterView);
814 [_engine updateWindowMetricsForViewController:self];
817 - (BOOL)viewShouldAcceptFirstResponder:(NSView*)view {
818 FML_DCHECK(view == _flutterView);
822 return !
self.activeTextInputPlugin.isFirstResponder;
825 #pragma mark - FlutterPluginRegistry
828 return [_engine registrarForPlugin:pluginName];
831 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
832 return [_engine valuePublishedByPlugin:pluginKey];
835 #pragma mark - FlutterKeyboardViewDelegate
837 - (BOOL)onTextInputKeyEvent:(nonnull NSEvent*)event {
838 return [
self.activeTextInputPlugin handleKeyEvent:event];
841 #pragma mark - NSResponder
843 - (BOOL)acceptsFirstResponder {
847 - (void)keyDown:(NSEvent*)event {
848 [_engine.keyboardManager handleEvent:event withContext:self];
851 - (void)keyUp:(NSEvent*)event {
852 [_engine.keyboardManager handleEvent:event withContext:self];
855 - (void)flagsChanged:(NSEvent*)event {
856 [_engine.keyboardManager handleEvent:event withContext:self];
859 - (void)mouseEntered:(NSEvent*)event {
860 if (_mouseState.has_pending_exit) {
861 _mouseState.has_pending_exit =
false;
863 [
self dispatchMouseEvent:event phase:kAdd];
867 - (void)mouseExited:(NSEvent*)event {
868 if (_mouseState.buttons != 0) {
869 _mouseState.has_pending_exit =
true;
872 [
self dispatchMouseEvent:event phase:kRemove];
875 - (void)mouseDown:(NSEvent*)event {
876 _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
877 [
self dispatchMouseEvent:event];
880 - (void)mouseUp:(NSEvent*)event {
881 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
882 [
self dispatchMouseEvent:event];
885 - (void)mouseDragged:(NSEvent*)event {
886 [
self dispatchMouseEvent:event];
889 - (void)rightMouseDown:(NSEvent*)event {
890 _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
891 [
self dispatchMouseEvent:event];
894 - (void)rightMouseUp:(NSEvent*)event {
895 _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
896 [
self dispatchMouseEvent:event];
899 - (void)rightMouseDragged:(NSEvent*)event {
900 [
self dispatchMouseEvent:event];
903 - (void)otherMouseDown:(NSEvent*)event {
904 _mouseState.buttons |= (1 <<
event.buttonNumber);
905 [
self dispatchMouseEvent:event];
908 - (void)otherMouseUp:(NSEvent*)event {
909 _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
910 [
self dispatchMouseEvent:event];
913 - (void)otherMouseDragged:(NSEvent*)event {
914 [
self dispatchMouseEvent:event];
917 - (void)mouseMoved:(NSEvent*)event {
918 [
self dispatchMouseEvent:event];
921 - (void)scrollWheel:(NSEvent*)event {
922 [
self dispatchGestureEvent:event];
925 - (void)magnifyWithEvent:(NSEvent*)event {
926 [
self dispatchGestureEvent:event];
929 - (void)rotateWithEvent:(NSEvent*)event {
930 [
self dispatchGestureEvent:event];
933 - (void)swipeWithEvent:(NSEvent*)event {
937 - (void)touchesBeganWithEvent:(NSEvent*)event {
938 NSTouch* touch =
event.allTouches.anyObject;
940 if ((event.timestamp - _mouseState.last_scroll_momentum_changed_time) <
941 kTrackpadTouchInertiaCancelWindowMs) {
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,
952 .device = kPointerPanZoomDeviceId,
953 .signal_kind = kFlutterPointerSignalKindScrollInertiaCancel,
954 .device_kind = kFlutterPointerDeviceKindTrackpad,
958 [_engine sendPointerEvent:flutterEvent];
960 _mouseState.last_scroll_momentum_changed_time = 0;
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)