• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
9#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterCodecs.h"
10#import "flutter/shell/platform/darwin/macos/framework/Headers/FlutterEngine.h"
11#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h"
12#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterTextInputPlugin.h"
13#import "flutter/shell/platform/darwin/macos/framework/Source/FlutterView.h"
14#import "flutter/shell/platform/embedder/embedder.h"
15
16namespace {
17
18/// Clipboard plain text format.
19constexpr char kTextPlainFormat[] = "text/plain";
20
21/**
22 * State tracking for mouse events, to adapt between the events coming from the system and the
23 * events that the embedding API expects.
24 */
25struct MouseState {
26  /**
27   * Whether or not a kAdd event has been sent (or sent again since the last kRemove if tracking is
28   * enabled). Used to determine whether to send a kAdd event before sending an incoming mouse
29   * event, since Flutter expects pointers to be added before events are sent for them.
30   */
31  bool flutter_state_is_added = false;
32
33  /**
34   * Whether or not a kDown has been sent since the last kAdd/kUp.
35   */
36  bool flutter_state_is_down = false;
37
38  /**
39   * Whether or not mouseExited: was received while a button was down. Cocoa's behavior when
40   * dragging out of a tracked area is to send an exit, then keep sending drag events until the last
41   * button is released. If it was released inside the view, mouseEntered: is sent the next time the
42   * mouse moves. Flutter doesn't expect to receive events after a kRemove, so the kRemove for the
43   * exit needs to be delayed until after the last mouse button is released.
44   */
45  bool has_pending_exit = false;
46
47  /**
48   * The currently pressed buttons, as represented in FlutterPointerEvent.
49   */
50  int64_t buttons = 0;
51
52  /**
53   * Resets all state to default values.
54   */
55  void Reset() {
56    flutter_state_is_added = false;
57    flutter_state_is_down = false;
58    has_pending_exit = false;
59    buttons = 0;
60  }
61};
62
63}  // namespace
64
65#pragma mark - Private interface declaration.
66
67/**
68 * Private interface declaration for FlutterViewController.
69 */
70@interface FlutterViewController () <FlutterViewReshapeListener>
71
72/**
73 * A list of additional responders to keyboard events. Keybord events are forwarded to all of them.
74 */
75@property(nonatomic) NSMutableOrderedSet<NSResponder*>* additionalKeyResponders;
76
77/**
78 * The tracking area used to generate hover events, if enabled.
79 */
80@property(nonatomic) NSTrackingArea* trackingArea;
81
82/**
83 * The current state of the mouse and the sent mouse events.
84 */
85@property(nonatomic) MouseState mouseState;
86
87/**
88 * Starts running |engine|, including any initial setup.
89 */
90- (BOOL)launchEngine;
91
92/**
93 * Updates |trackingArea| for the current tracking settings, creating it with
94 * the correct mode if tracking is enabled, or removing it if not.
95 */
96- (void)configureTrackingArea;
97
98/**
99 * Creates and registers plugins used by this view controller.
100 */
101- (void)addInternalPlugins;
102
103/**
104 * Calls dispatchMouseEvent:phase: with a phase determined by self.mouseState.
105 *
106 * mouseState.buttons should be updated before calling this method.
107 */
108- (void)dispatchMouseEvent:(nonnull NSEvent*)event;
109
110/**
111 * Converts |event| to a FlutterPointerEvent with the given phase, and sends it to the engine.
112 */
113- (void)dispatchMouseEvent:(nonnull NSEvent*)event phase:(FlutterPointerPhase)phase;
114
115/**
116 * Converts |event| to a key event channel message, and sends it to the engine.
117 */
118- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type;
119
120/**
121 * Initializes the KVO for user settings and passes the initial user settings to the engine.
122 */
123- (void)sendInitialSettings;
124
125/**
126 * Responsds to updates in the user settings and passes this data to the engine.
127 */
128- (void)onSettingsChanged:(NSNotification*)notification;
129
130/**
131 * Handles messages received from the Flutter engine on the _*Channel channels.
132 */
133- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
134
135/**
136 * Reads the data from the clipboard. |format| specifies the media type of the
137 * data to obtain.
138 */
139- (NSDictionary*)getClipboardData:(NSString*)format;
140
141/**
142 * Clears contents and writes new data into clipboard. |data| is a dictionary where
143 * the keys are the type of data, and tervalue the data to be stored.
144 */
145- (void)setClipboardData:(NSDictionary*)data;
146
147@end
148
149#pragma mark - FlutterViewController implementation.
150
151@implementation FlutterViewController {
152  // The project to run in this controller's engine.
153  FlutterDartProject* _project;
154
155  // The plugin used to handle text input. This is not an FlutterPlugin, so must be owned
156  // separately.
157  FlutterTextInputPlugin* _textInputPlugin;
158
159  // A message channel for passing key events to the Flutter engine. This should be replaced with
160  // an embedding API; see Issue #47.
161  FlutterBasicMessageChannel* _keyEventChannel;
162
163  // A message channel for sending user settings to the flutter engine.
164  FlutterBasicMessageChannel* _settingsChannel;
165
166  // A method channel for miscellaneous platform functionality.
167  FlutterMethodChannel* _platformChannel;
168}
169
170@dynamic view;
171
172/**
173 * Performs initialization that's common between the different init paths.
174 */
175static void CommonInit(FlutterViewController* controller) {
176  controller->_engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
177                                                    project:controller->_project
178                                     allowHeadlessExecution:NO];
179  controller->_additionalKeyResponders = [[NSMutableOrderedSet alloc] init];
180  controller->_mouseTrackingMode = FlutterMouseTrackingModeInKeyWindow;
181}
182
183- (instancetype)initWithCoder:(NSCoder*)coder {
184  self = [super initWithCoder:coder];
185  NSAssert(self, @"Super init cannot be nil");
186
187  CommonInit(self);
188  return self;
189}
190
191- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
192  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
193  NSAssert(self, @"Super init cannot be nil");
194
195  CommonInit(self);
196  return self;
197}
198
199- (instancetype)initWithProject:(nullable FlutterDartProject*)project {
200  self = [super initWithNibName:nil bundle:nil];
201  NSAssert(self, @"Super init cannot be nil");
202
203  _project = project;
204  CommonInit(self);
205  return self;
206}
207
208- (void)loadView {
209  NSOpenGLContext* resourceContext = _engine.resourceContext;
210  if (!resourceContext) {
211    NSLog(@"Unable to create FlutterView; no resource context available.");
212    return;
213  }
214  FlutterView* flutterView = [[FlutterView alloc] initWithShareContext:resourceContext
215                                                       reshapeListener:self];
216  self.view = flutterView;
217}
218
219- (void)viewDidLoad {
220  [self configureTrackingArea];
221}
222
223- (void)viewWillAppear {
224  [super viewWillAppear];
225  if (!_engine.running) {
226    [self launchEngine];
227  }
228}
229
230- (void)dealloc {
231  _engine.viewController = nil;
232}
233
234#pragma mark - Public methods
235
236- (void)setMouseTrackingMode:(FlutterMouseTrackingMode)mode {
237  if (_mouseTrackingMode == mode) {
238    return;
239  }
240  _mouseTrackingMode = mode;
241  [self configureTrackingArea];
242}
243
244#pragma mark - Framework-internal methods
245
246- (FlutterView*)flutterView {
247  return static_cast<FlutterView*>(self.view);
248}
249
250- (void)addKeyResponder:(NSResponder*)responder {
251  [self.additionalKeyResponders addObject:responder];
252}
253
254- (void)removeKeyResponder:(NSResponder*)responder {
255  [self.additionalKeyResponders removeObject:responder];
256}
257
258#pragma mark - Private methods
259
260- (BOOL)launchEngine {
261  // Register internal plugins before starting the engine.
262  [self addInternalPlugins];
263
264  _engine.viewController = self;
265  if (![_engine runWithEntrypoint:nil]) {
266    return NO;
267  }
268  // Send the initial user settings such as brightness and text scale factor
269  // to the engine.
270  [self sendInitialSettings];
271  return YES;
272}
273
274- (void)configureTrackingArea {
275  if (_mouseTrackingMode != FlutterMouseTrackingModeNone && self.view) {
276    NSTrackingAreaOptions options =
277        NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingInVisibleRect;
278    switch (_mouseTrackingMode) {
279      case FlutterMouseTrackingModeInKeyWindow:
280        options |= NSTrackingActiveInKeyWindow;
281        break;
282      case FlutterMouseTrackingModeInActiveApp:
283        options |= NSTrackingActiveInActiveApp;
284        break;
285      case FlutterMouseTrackingModeAlways:
286        options |= NSTrackingActiveAlways;
287        break;
288      default:
289        NSLog(@"Error: Unrecognized mouse tracking mode: %ld", _mouseTrackingMode);
290        return;
291    }
292    _trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect
293                                                 options:options
294                                                   owner:self
295                                                userInfo:nil];
296    [self.view addTrackingArea:_trackingArea];
297  } else if (_trackingArea) {
298    [self.view removeTrackingArea:_trackingArea];
299    _trackingArea = nil;
300  }
301}
302
303- (void)addInternalPlugins {
304  _textInputPlugin = [[FlutterTextInputPlugin alloc] initWithViewController:self];
305  _keyEventChannel =
306      [FlutterBasicMessageChannel messageChannelWithName:@"flutter/keyevent"
307                                         binaryMessenger:_engine.binaryMessenger
308                                                   codec:[FlutterJSONMessageCodec sharedInstance]];
309  _settingsChannel =
310      [FlutterBasicMessageChannel messageChannelWithName:@"flutter/settings"
311                                         binaryMessenger:_engine.binaryMessenger
312                                                   codec:[FlutterJSONMessageCodec sharedInstance]];
313  _platformChannel =
314      [FlutterMethodChannel methodChannelWithName:@"flutter/platform"
315                                  binaryMessenger:_engine.binaryMessenger
316                                            codec:[FlutterJSONMethodCodec sharedInstance]];
317  __weak FlutterViewController* weakSelf = self;
318  [_platformChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
319    [weakSelf handleMethodCall:call result:result];
320  }];
321}
322
323- (void)dispatchMouseEvent:(nonnull NSEvent*)event {
324  FlutterPointerPhase phase = _mouseState.buttons == 0
325                                  ? (_mouseState.flutter_state_is_down ? kUp : kHover)
326                                  : (_mouseState.flutter_state_is_down ? kMove : kDown);
327  [self dispatchMouseEvent:event phase:phase];
328}
329
330- (void)dispatchMouseEvent:(NSEvent*)event phase:(FlutterPointerPhase)phase {
331  // There are edge cases where the system will deliver enter out of order relative to other
332  // events (e.g., drag out and back in, release, then click; mouseDown: will be called before
333  // mouseEntered:). Discard those events, since the add will already have been synthesized.
334  if (_mouseState.flutter_state_is_added && phase == kAdd) {
335    return;
336  }
337
338  // If a pointer added event hasn't been sent, synthesize one using this event for the basic
339  // information.
340  if (!_mouseState.flutter_state_is_added && phase != kAdd) {
341    // Only the values extracted for use in flutterEvent below matter, the rest are dummy values.
342    NSEvent* addEvent = [NSEvent enterExitEventWithType:NSEventTypeMouseEntered
343                                               location:event.locationInWindow
344                                          modifierFlags:0
345                                              timestamp:event.timestamp
346                                           windowNumber:event.windowNumber
347                                                context:nil
348                                            eventNumber:0
349                                         trackingNumber:0
350                                               userData:NULL];
351    [self dispatchMouseEvent:addEvent phase:kAdd];
352  }
353
354  NSPoint locationInView = [self.view convertPoint:event.locationInWindow fromView:nil];
355  NSPoint locationInBackingCoordinates = [self.view convertPointToBacking:locationInView];
356  FlutterPointerEvent flutterEvent = {
357      .struct_size = sizeof(flutterEvent),
358      .device_kind = kFlutterPointerDeviceKindMouse,
359      .phase = phase,
360      .x = locationInBackingCoordinates.x,
361      .y = -locationInBackingCoordinates.y,  // convertPointToBacking makes this negative.
362      .timestamp = static_cast<size_t>(event.timestamp * NSEC_PER_MSEC),
363      // If a click triggered a synthesized kAdd, don't pass the buttons in that event.
364      .buttons = phase == kAdd ? 0 : _mouseState.buttons,
365  };
366
367  if (event.type == NSEventTypeScrollWheel) {
368    flutterEvent.signal_kind = kFlutterPointerSignalKindScroll;
369
370    double pixelsPerLine = 1.0;
371    if (!event.hasPreciseScrollingDeltas) {
372      CGEventSourceRef source = CGEventCreateSourceFromEvent(event.CGEvent);
373      pixelsPerLine = CGEventSourceGetPixelsPerLine(source);
374      if (source) {
375        CFRelease(source);
376      }
377    }
378    double scaleFactor = self.view.layer.contentsScale;
379    flutterEvent.scroll_delta_x = -event.scrollingDeltaX * pixelsPerLine * scaleFactor;
380    flutterEvent.scroll_delta_y = -event.scrollingDeltaY * pixelsPerLine * scaleFactor;
381  }
382  [_engine sendPointerEvent:flutterEvent];
383
384  // Update tracking of state as reported to Flutter.
385  if (phase == kDown) {
386    _mouseState.flutter_state_is_down = true;
387  } else if (phase == kUp) {
388    _mouseState.flutter_state_is_down = false;
389    if (_mouseState.has_pending_exit) {
390      [self dispatchMouseEvent:event phase:kRemove];
391      _mouseState.has_pending_exit = false;
392    }
393  } else if (phase == kAdd) {
394    _mouseState.flutter_state_is_added = true;
395  } else if (phase == kRemove) {
396    _mouseState.Reset();
397  }
398}
399
400- (void)dispatchKeyEvent:(NSEvent*)event ofType:(NSString*)type {
401  [_keyEventChannel sendMessage:@{
402    @"keymap" : @"macos",
403    @"type" : type,
404    @"keyCode" : @(event.keyCode),
405    @"modifiers" : @(event.modifierFlags),
406    @"characters" : event.characters,
407    @"charactersIgnoringModifiers" : event.charactersIgnoringModifiers,
408  }];
409}
410
411- (void)onSettingsChanged:(NSNotification*)notification {
412  // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015.
413  NSString* brightness =
414      [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"];
415  [_settingsChannel sendMessage:@{
416    @"platformBrightness" : [brightness isEqualToString:@"Dark"] ? @"dark" : @"light",
417    // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32006.
418    @"textScaleFactor" : @1.0,
419    @"alwaysUse24HourFormat" : @false
420  }];
421}
422
423- (void)sendInitialSettings {
424  // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/32015.
425  [[NSDistributedNotificationCenter defaultCenter]
426      addObserver:self
427         selector:@selector(onSettingsChanged:)
428             name:@"AppleInterfaceThemeChangedNotification"
429           object:nil];
430  [self onSettingsChanged:nil];
431}
432
433- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
434  if ([call.method isEqualToString:@"SystemNavigator.pop"]) {
435    [NSApp terminate:self];
436    result(nil);
437  } else if ([call.method isEqualToString:@"Clipboard.getData"]) {
438    result([self getClipboardData:call.arguments]);
439  } else if ([call.method isEqualToString:@"Clipboard.setData"]) {
440    [self setClipboardData:call.arguments];
441    result(nil);
442  } else {
443    result(FlutterMethodNotImplemented);
444  }
445}
446
447- (NSDictionary*)getClipboardData:(NSString*)format {
448  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
449  if ([format isEqualToString:@(kTextPlainFormat)]) {
450    NSString* stringInPasteboard = [pasteboard stringForType:NSPasteboardTypeString];
451    return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard};
452  }
453  return nil;
454}
455
456- (void)setClipboardData:(NSDictionary*)data {
457  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
458  NSString* text = data[@"text"];
459  if (text && ![text isEqual:[NSNull null]]) {
460    [pasteboard clearContents];
461    [pasteboard setString:text forType:NSPasteboardTypeString];
462  }
463}
464
465#pragma mark - FlutterViewReshapeListener
466
467/**
468 * Responds to view reshape by notifying the engine of the change in dimensions.
469 */
470- (void)viewDidReshape:(NSView*)view {
471  [_engine updateWindowMetrics];
472}
473
474#pragma mark - FlutterPluginRegistry
475
476- (id<FlutterPluginRegistrar>)registrarForPlugin:(NSString*)pluginName {
477  return [_engine registrarForPlugin:pluginName];
478}
479
480#pragma mark - NSResponder
481
482- (BOOL)acceptsFirstResponder {
483  return YES;
484}
485
486- (void)keyDown:(NSEvent*)event {
487  [self dispatchKeyEvent:event ofType:@"keydown"];
488  for (NSResponder* responder in self.additionalKeyResponders) {
489    if ([responder respondsToSelector:@selector(keyDown:)]) {
490      [responder keyDown:event];
491    }
492  }
493}
494
495- (void)keyUp:(NSEvent*)event {
496  [self dispatchKeyEvent:event ofType:@"keyup"];
497  for (NSResponder* responder in self.additionalKeyResponders) {
498    if ([responder respondsToSelector:@selector(keyUp:)]) {
499      [responder keyUp:event];
500    }
501  }
502}
503
504- (void)mouseEntered:(NSEvent*)event {
505  [self dispatchMouseEvent:event phase:kAdd];
506}
507
508- (void)mouseExited:(NSEvent*)event {
509  if (_mouseState.buttons != 0) {
510    _mouseState.has_pending_exit = true;
511    return;
512  }
513  [self dispatchMouseEvent:event phase:kRemove];
514}
515
516- (void)mouseDown:(NSEvent*)event {
517  _mouseState.buttons |= kFlutterPointerButtonMousePrimary;
518  [self dispatchMouseEvent:event];
519}
520
521- (void)mouseUp:(NSEvent*)event {
522  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMousePrimary);
523  [self dispatchMouseEvent:event];
524}
525
526- (void)mouseDragged:(NSEvent*)event {
527  [self dispatchMouseEvent:event];
528}
529
530- (void)rightMouseDown:(NSEvent*)event {
531  _mouseState.buttons |= kFlutterPointerButtonMouseSecondary;
532  [self dispatchMouseEvent:event];
533}
534
535- (void)rightMouseUp:(NSEvent*)event {
536  _mouseState.buttons &= ~static_cast<uint64_t>(kFlutterPointerButtonMouseSecondary);
537  [self dispatchMouseEvent:event];
538}
539
540- (void)rightMouseDragged:(NSEvent*)event {
541  [self dispatchMouseEvent:event];
542}
543
544- (void)otherMouseDown:(NSEvent*)event {
545  _mouseState.buttons |= (1 << event.buttonNumber);
546  [self dispatchMouseEvent:event];
547}
548
549- (void)otherMouseUp:(NSEvent*)event {
550  _mouseState.buttons &= ~static_cast<uint64_t>(1 << event.buttonNumber);
551  [self dispatchMouseEvent:event];
552}
553
554- (void)otherMouseDragged:(NSEvent*)event {
555  [self dispatchMouseEvent:event];
556}
557
558- (void)mouseMoved:(NSEvent*)event {
559  [self dispatchMouseEvent:event];
560}
561
562- (void)scrollWheel:(NSEvent*)event {
563  // TODO: Add gesture-based (trackpad) scroll support once it's supported by the engine rather
564  // than always using kHover.
565  [self dispatchMouseEvent:event phase:kHover];
566}
567
568@end
569