Flutter macOS Embedder
FlutterWindowController.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 
6 #include <Foundation/Foundation.h>
7 
12 
14 
15 // A delegate for a Flutter managed window.
16 @interface FlutterWindowOwner : NSObject <NSWindowDelegate, FlutterViewSizingDelegate> {
17  // Strong reference to the window. This is the only strong reference to the
18  // window.
19  NSWindow* _window;
21  std::optional<flutter::Isolate> _isolate;
23 
24  // Extra size constraints coming from the window positioner.
26 }
27 
28 @property(readonly, nonatomic) NSWindow* window;
29 @property(readonly, nonatomic) FlutterViewController* flutterViewController;
30 @property(readwrite, nonatomic) BOOL closeWhenParentResignsKey;
31 
32 - (instancetype)initWithWindow:(NSWindow*)window
33  flutterViewController:(FlutterViewController*)viewController
34  creationRequest:(const FlutterWindowCreationRequest&)creationRequest;
35 
36 @end
37 
39 
40 - (void)flutterSetContentSize:(FlutterWindowSize)contentSize;
41 - (void)flutterSetConstraints:(FlutterWindowConstraints)constraints;
42 
43 @end
44 
45 @implementation NSWindow (FlutterWindowSizing)
46 - (void)flutterSetContentSize:(FlutterWindowSize)contentSize {
47  [self setContentSize:NSMakeSize(contentSize.width, contentSize.height)];
48 }
49 
50 - (void)flutterSetConstraints:(FlutterWindowConstraints)constraints {
51  NSSize size = [self frameRectForContentRect:self.frame].size;
52  NSSize originalSize = size;
53  [self setContentMinSize:NSMakeSize(constraints.min_width, constraints.min_height)];
54  size.width = std::max(size.width, constraints.min_width);
55  size.height = std::max(size.height, constraints.min_height);
56  if (constraints.max_width > 0 && constraints.max_height > 0) {
57  [self setContentMaxSize:NSMakeSize(constraints.max_width, constraints.max_height)];
58  size.width = std::min(size.width, constraints.max_width);
59  size.height = std::min(size.height, constraints.max_height);
60  } else {
61  [self setContentMaxSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)];
62  }
63  if (!NSEqualSizes(originalSize, size)) {
64  [self setContentSize:size];
65  }
66 }
67 
68 @end
69 
70 @interface FlutterWindowController () {
71  NSMutableArray<FlutterWindowOwner*>* _windows;
72 }
73 
74 - (void)windowDidResignKey:(FlutterWindowOwner*)window;
75 
76 @end
77 
78 @implementation FlutterWindowOwner
79 
80 @synthesize window = _window;
82 
83 - (instancetype)initWithWindow:(NSWindow*)window
84  flutterViewController:(FlutterViewController*)viewController
85  creationRequest:(const FlutterWindowCreationRequest&)creationRequest {
86  if (self = [super init]) {
87  _window = window;
88  _flutterViewController = viewController;
89  _creationRequest = creationRequest;
91  }
92  return self;
93 }
94 
95 - (void)windowDidBecomeKey:(NSNotification*)notification {
96  [_flutterViewController.engine windowDidBecomeKey:_flutterViewController.viewIdentifier];
97 }
98 
99 - (void)windowDidResignKey:(NSNotification*)notification {
100  [_flutterViewController.engine windowDidResignKey:_flutterViewController.viewIdentifier];
101  [[_flutterViewController.engine windowController] windowDidResignKey:self];
102 }
103 
104 - (BOOL)windowShouldClose:(NSWindow*)sender {
105  flutter::IsolateScope isolate_scope(*_isolate);
107  return NO;
108 }
109 
110 - (void)windowWillClose {
112 }
113 
114 - (void)windowDidResize:(NSNotification*)notification {
115  flutter::IsolateScope isolate_scope(*_isolate);
117 }
118 
119 // Miniaturize does not trigger resize event, but for now there
120 // is no other way to get notification about the state change.
121 - (void)windowDidMiniaturize:(NSNotification*)notification {
122  flutter::IsolateScope isolate_scope(*_isolate);
124 }
125 
126 // Deminiaturize does not trigger resize event, but for now there
127 // is no other way to get notification about the state change.
128 - (void)windowDidDeminiaturize:(NSNotification*)notification {
129  flutter::IsolateScope isolate_scope(*_isolate);
131 }
132 
133 - (void)windowWillEnterFullScreen:(NSNotification*)notification {
134  flutter::IsolateScope isolate_scope(*_isolate);
136 }
137 
138 - (void)windowWillExitFullScreen:(NSNotification*)notification {
139  flutter::IsolateScope isolate_scope(*_isolate);
141 }
142 
143 - (std::optional<NSSize>)minimumViewSize:(FlutterView*)view {
145  return NSMakeSize(_creationRequest.constraints.min_width,
147  } else {
148  return std::nullopt;
149  }
150 }
151 
152 - (std::optional<NSSize>)maximumViewSize:(FlutterView*)view {
154  // Window is not sized to contents.
155  return std::nullopt;
156  }
157  NSSize screenSize = self.window.screen.visibleFrame.size;
158  double width = screenSize.width;
159  width = std::min(width, _creationRequest.constraints.max_width);
160  if (_positionerSizeConstraints.width > 0) {
161  width = std::min(width, _positionerSizeConstraints.width);
162  }
163  double height = screenSize.height;
164  height = std::min(height, _creationRequest.constraints.max_height);
165  if (_positionerSizeConstraints.height > 0) {
166  height = std::min(height, _positionerSizeConstraints.height);
167  }
168  return NSMakeSize(width, height);
169 }
170 
171 // Returns the frame that includes all screen. This is used to flip coordinates
172 // of individual screen to match Flutter coordinate system.
173 static NSRect ComputeGlobalScreenFrame() {
174  NSRect frame = NSZeroRect;
175  for (NSScreen* screen in [NSScreen screens]) {
176  NSRect screenFrame = screen.frame;
177  if (NSIsEmptyRect(frame)) {
178  frame = screenFrame;
179  } else {
180  frame = NSUnionRect(frame, screenFrame);
181  }
182  }
183  return frame;
184 }
185 
186 static void FlipRect(NSRect& rect, const NSRect& globalScreenFrame) {
187  // Flip the y coordinate to match Flutter coordinate system.
188  rect.origin.y = (globalScreenFrame.origin.y + globalScreenFrame.size.height) -
189  (rect.origin.y + rect.size.height);
190 }
191 
192 - (void)updatePosition {
193  [self viewDidUpdateContents:self.flutterViewController.flutterView
194  withSize:self.flutterViewController.flutterView.bounds.size];
195 }
196 
197 - (void)viewDidUpdateContents:(FlutterView*)view withSize:(NSSize)newSize {
198  if (_creationRequest.on_get_window_position == nullptr) {
199  // There is no positioner associated with this window.
200  return;
201  }
202 
203  NSRect globalScreenFrame = ComputeGlobalScreenFrame();
204 
205  NSRect parentRect =
206  [self.window.parentWindow contentRectForFrameRect:self.window.parentWindow.frame];
207  FlipRect(parentRect, globalScreenFrame);
208 
209  NSRect screenRect = [self.window.screen visibleFrame];
210  FlipRect(screenRect, globalScreenFrame);
211 
212  flutter::IsolateScope isolate_scope(*_isolate);
215  FlutterWindowRect::fromNSRect(screenRect));
216 
217  NSRect positionRect = position->toNSRect();
218  FlipRect(positionRect, globalScreenFrame);
219 
220  [self.window setFrame:positionRect display:NO animate:NO];
221 
222  free(position);
223 
224  // For windows sized to contents if the positioner size doesn't match actual size
225  // the requested size needs to be passed through constraints.
226  if (view.sizedToContents &&
227  (positionRect.size.width < newSize.width || positionRect.size.height < newSize.height)) {
228  _positionerSizeConstraints = positionRect.size;
229  [view constraintsDidChange];
230  } else {
231  // Only show the window initially if positioner agrees with the size.
232  self.window.alphaValue = 1.0;
233  }
234 }
235 
236 - (void)setConstraints:(FlutterWindowConstraints)constraints {
237  if (_flutterViewController.flutterView.sizedToContents) {
238  self->_creationRequest.constraints = constraints;
239  [_flutterViewController.flutterView constraintsDidChange];
240  } else {
241  [self.window flutterSetConstraints:constraints];
242  }
243 }
244 
245 @end
246 
247 @implementation FlutterWindowController
248 
249 - (instancetype)init {
250  self = [super init];
251  if (self != nil) {
252  _windows = [NSMutableArray array];
253  }
254  return self;
255 }
256 
257 - (FlutterViewIdentifier)createDialogWindow:(const FlutterWindowCreationRequest*)request {
258  FlutterViewController* controller = [[FlutterViewController alloc] initWithEngine:_engine
259  nibName:nil
260  bundle:nil];
261 
262  NSWindow* window = [[NSWindow alloc] init];
263  // If this is not set there will be double free on window close when
264  // using ARC.
265  [window setReleasedWhenClosed:NO];
266 
267  window.contentViewController = controller;
268  window.styleMask =
269  NSWindowStyleMaskResizable | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable;
270  window.collectionBehavior = NSWindowCollectionBehaviorFullScreenAuxiliary;
271  if (request->has_size) {
272  [window flutterSetContentSize:request->size];
273  }
274  if (request->has_constraints) {
275  [window flutterSetConstraints:request->constraints];
276  }
277 
278  FlutterWindowOwner* owner = [[FlutterWindowOwner alloc] initWithWindow:window
279  flutterViewController:controller
280  creationRequest:*request];
281  window.delegate = owner;
282  [_windows addObject:owner];
283 
284  NSWindow* parent = nil;
285 
286  if (request->parent_view_id != 0) {
287  for (FlutterWindowOwner* owner in _windows) {
288  if (owner.flutterViewController.viewIdentifier == request->parent_view_id) {
289  parent = owner.window;
290  break;
291  }
292  }
293  if (parent == nil) {
294  FML_LOG(WARNING) << "Failed to find parent window for ID " << request->parent_view_id;
295  }
296  }
297 
298  if (parent != nil) {
299  dispatch_async(dispatch_get_main_queue(), ^{
300  // beginCriticalSheet blocks with nested run loop until the
301  // sheet animation is finished.
302  [parent beginCriticalSheet:window
303  completionHandler:^(NSModalResponse response){
304  }];
305  });
306 
307  } else {
308  [window setIsVisible:YES];
309  [window makeKeyAndOrderFront:nil];
310  }
311 
312  return controller.viewIdentifier;
313 }
314 
315 - (FlutterViewIdentifier)createTooltipWindow:(const FlutterWindowCreationRequest*)request {
316  FlutterViewController* controller = [[FlutterViewController alloc] initWithEngine:_engine
317  nibName:nil
318  bundle:nil];
319 
320  NSWindow* window = [[NSWindow alloc] init];
321  // If this is not set there will be double free on window close when
322  // using ARC.
323  [window setReleasedWhenClosed:NO];
324 
325  window.contentViewController = controller;
326  window.styleMask = NSWindowStyleMaskBorderless;
327  window.hasShadow = NO;
328  window.opaque = NO;
329  window.backgroundColor = [NSColor clearColor];
330 
331  FlutterWindowOwner* owner = [[FlutterWindowOwner alloc] initWithWindow:window
332  flutterViewController:controller
333  creationRequest:*request];
334 
335  controller.flutterView.sizingDelegate = owner;
336  controller.flutterView.backgroundColor = [NSColor clearColor];
337  // Resend configure event after setting the sizing delegate.
338  [controller.flutterView constraintsDidChange];
339  owner.closeWhenParentResignsKey = YES;
340 
341  window.delegate = owner;
342  [_windows addObject:owner];
343 
344  NSWindow* parent = nil;
345 
346  if (request->parent_view_id != 0) {
347  for (FlutterWindowOwner* owner in _windows) {
348  if (owner.flutterViewController.viewIdentifier == request->parent_view_id) {
349  parent = owner.window;
350  break;
351  }
352  }
353  }
354 
355  NSAssert(parent != nil, @"Tooltip window must have a parent window.");
356 
357  window.ignoresMouseEvents = YES;
358  window.collectionBehavior = NSWindowCollectionBehaviorAuxiliary;
359  [parent addChildWindow:window ordered:NSWindowAbove];
360  window.alphaValue = 0.0;
361  return controller.viewIdentifier;
362 }
363 
364 - (FlutterViewIdentifier)createRegularWindow:(const FlutterWindowCreationRequest*)request {
365  FlutterViewController* controller = [[FlutterViewController alloc] initWithEngine:_engine
366  nibName:nil
367  bundle:nil];
368 
369  NSWindow* window = [[NSWindow alloc] init];
370  // If this is not set there will be double free on window close when
371  // using ARC.
372  [window setReleasedWhenClosed:NO];
373 
374  window.contentViewController = controller;
375  window.styleMask = NSWindowStyleMaskResizable | NSWindowStyleMaskTitled |
376  NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
377  if (request->has_size) {
378  [window flutterSetContentSize:request->size];
379  }
380  if (request->has_constraints) {
381  [window flutterSetConstraints:request->constraints];
382  }
383  [window setIsVisible:YES];
384  [window makeKeyAndOrderFront:nil];
385 
386  FlutterWindowOwner* owner = [[FlutterWindowOwner alloc] initWithWindow:window
387  flutterViewController:controller
388  creationRequest:*request];
389  window.delegate = owner;
390  [_windows addObject:owner];
391 
392  return controller.viewIdentifier;
393 }
394 
395 - (void)destroyWindow:(NSWindow*)window {
396  FlutterWindowOwner* owner = nil;
397  for (FlutterWindowOwner* o in _windows) {
398  if (o.window == window) {
399  owner = o;
400  break;
401  }
402  }
403  if (owner != nil) {
404  [_windows removeObject:owner];
405  for (NSWindow* win in owner.window.sheets) {
406  [self destroyWindow:win];
407  }
408 
409  for (NSWindow* win in owner.window.childWindows) {
410  [self destroyWindow:win];
411  }
412  // Make sure to unregister the controller from the engine and remove the FlutterView
413  // before destroying the window and Flutter NSView.
414  [owner.flutterViewController dispose];
415  owner.window.delegate = nil;
416  [owner.window close];
417  [owner windowWillClose];
418  }
419 }
420 
421 - (void)closeAllWindows {
422  for (FlutterWindowOwner* owner in _windows) {
423  [owner.flutterViewController dispose];
424  [owner.window close];
425  }
426  [_windows removeAllObjects];
427 }
428 
429 static BOOL IsChildAncestor(NSWindow* child, NSWindow* ancestor) {
430  NSWindow* current = child.parentWindow;
431  while (current) {
432  if (current == ancestor) {
433  return YES;
434  }
435  current = current.parentWindow;
436  }
437 
438  return NO;
439 }
440 
441 - (void)windowDidResignKey:(FlutterWindowOwner*)parent {
442  for (FlutterWindowOwner* possibleChild in _windows) {
443  if (possibleChild.closeWhenParentResignsKey &&
444  IsChildAncestor(possibleChild.window, parent.window)) {
445  [possibleChild windowShouldClose:possibleChild.window];
446  }
447  }
448 }
449 
450 @end
451 
452 // NOLINTBEGIN(google-objc-function-naming)
453 
455  int64_t engine_id,
456  const FlutterWindowCreationRequest* request) {
457  FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id];
458  [engine enableMultiView];
459  return [engine.windowController createRegularWindow:request];
460 }
461 
463  int64_t engine_id,
464  const FlutterWindowCreationRequest* request) {
465  FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id];
466  [engine enableMultiView];
467  return [engine.windowController createDialogWindow:request];
468 }
469 
471  int64_t engine_id,
472  const FlutterWindowCreationRequest* request) {
473  FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id];
474  [engine enableMultiView];
475  return [engine.windowController createTooltipWindow:request];
476 }
477 
478 void InternalFlutter_Window_Destroy(int64_t engine_id, void* window) {
479  NSWindow* w = (__bridge NSWindow*)window;
480  FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id];
481  [engine.windowController destroyWindow:w];
482 }
483 
484 void* InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id) {
485  FlutterEngine* engine = [FlutterEngine engineForIdentifier:engine_id];
486  FlutterViewController* controller = [engine viewControllerForIdentifier:view_id];
487  return (__bridge void*)controller.view.window;
488 }
489 
491  NSWindow* w = (__bridge NSWindow*)window;
492  NSRect contentRect = [w contentRectForFrameRect:w.frame];
493  return {
494  .width = contentRect.size.width,
495  .height = contentRect.size.height,
496  };
497 }
498 
500  NSWindow* w = (__bridge NSWindow*)window;
501  [w flutterSetContentSize:*size];
502 }
503 
506  const FlutterWindowConstraints* constraints) {
507  NSWindow* w = (__bridge NSWindow*)window;
508  FlutterWindowOwner* owner = (FlutterWindowOwner*)w.delegate;
509  [owner setConstraints:*constraints];
510 }
511 
512 void InternalFlutter_Window_SetTitle(void* window, const char* title) {
513  NSWindow* w = (__bridge NSWindow*)window;
514  w.title = [NSString stringWithUTF8String:title];
515 }
516 
517 void InternalFlutter_Window_SetMaximized(void* window, bool maximized) {
518  NSWindow* w = (__bridge NSWindow*)window;
519  if (maximized & !w.isZoomed) {
520  [w zoom:nil];
521  } else if (!maximized && w.isZoomed) {
522  [w zoom:nil];
523  }
524 }
525 
527  NSWindow* w = (__bridge NSWindow*)window;
528  return w.isZoomed;
529 }
530 
532  NSWindow* w = (__bridge NSWindow*)window;
533  [w miniaturize:nil];
534 }
535 
537  NSWindow* w = (__bridge NSWindow*)window;
538  [w deminiaturize:nil];
539 }
540 
542  NSWindow* w = (__bridge NSWindow*)window;
543  return w.isMiniaturized;
544 }
545 
546 void InternalFlutter_Window_SetFullScreen(void* window, bool fullScreen) {
547  NSWindow* w = (__bridge NSWindow*)window;
548  bool isFullScreen = (w.styleMask & NSWindowStyleMaskFullScreen) != 0;
549  if (fullScreen && !isFullScreen) {
550  [w toggleFullScreen:nil];
551  } else if (!fullScreen && isFullScreen) {
552  [w toggleFullScreen:nil];
553  }
554 }
555 
557  NSWindow* w = (__bridge NSWindow*)window;
558  return (w.styleMask & NSWindowStyleMaskFullScreen) != 0;
559 }
560 
562  NSWindow* w = (__bridge NSWindow*)window;
563  [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
564  [w makeKeyAndOrderFront:nil];
565 }
566 
567 char* InternalFlutter_Window_GetTitle(void* window) {
568  NSWindow* w = (__bridge NSWindow*)window;
569  return strdup(w.title.UTF8String);
570 }
571 
573  NSWindow* w = (__bridge NSWindow*)window;
574  return w.isKeyWindow;
575 }
576 
578  NSWindow* w = (__bridge NSWindow*)window;
579  FlutterWindowOwner* owner = (FlutterWindowOwner*)w.delegate;
580  [owner updatePosition];
581 }
582 
583 // NOLINTEND(google-objc-function-naming)
#define FLUTTER_DARWIN_EXPORT
Definition: FlutterMacros.h:14
int64_t FlutterViewIdentifier
void InternalFlutter_Window_Activate(void *window)
void * InternalFlutter_Window_GetHandle(int64_t engine_id, FlutterViewIdentifier view_id)
bool InternalFlutter_Window_IsMaximized(void *window)
int64_t InternalFlutter_WindowController_CreateTooltipWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
FLUTTER_DARWIN_EXPORT void InternalFlutter_Window_SetConstraints(void *window, const FlutterWindowConstraints *constraints)
bool InternalFlutter_Window_IsActivated(void *window)
int64_t InternalFlutter_WindowController_CreateDialogWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
int64_t InternalFlutter_WindowController_CreateRegularWindow(int64_t engine_id, const FlutterWindowCreationRequest *request)
void InternalFlutter_Window_SetFullScreen(void *window, bool fullScreen)
void InternalFlutter_Window_UpdatePosition(void *window)
bool InternalFlutter_Window_IsFullScreen(void *window)
void InternalFlutter_Window_SetContentSize(void *window, const FlutterWindowSize *size)
void InternalFlutter_Window_Minimize(void *window)
char * InternalFlutter_Window_GetTitle(void *window)
void InternalFlutter_Window_SetTitle(void *window, const char *title)
bool InternalFlutter_Window_IsMinimized(void *window)
void InternalFlutter_Window_Destroy(int64_t engine_id, void *window)
void InternalFlutter_Window_SetMaximized(void *window, bool maximized)
FlutterWindowSize InternalFlutter_Window_GetContentSize(void *window)
void InternalFlutter_Window_Unminimize(void *window)
NSMutableArray< FlutterWindowOwner * > * _windows
static Isolate Current()
Definition: isolate_scope.cc:9
FlutterViewIdentifier viewIdentifier
BOOL sizedToContents
Definition: FlutterView.h:121
void constraintsDidChange()
Definition: FlutterView.mm:200
FlutterViewController * _flutterViewController
FlutterViewController * flutterViewController
std::optional< flutter::Isolate > _isolate
FlutterWindowCreationRequest _creationRequest
struct FlutterWindowConstraints constraints
FlutterWindowRect *(* on_get_window_position)(const FlutterWindowSize &child_size, const FlutterWindowRect &parent_rect, const FlutterWindowRect &output_rect)
static FlutterWindowRect fromNSRect(const NSRect &rect)
static FlutterWindowSize fromNSSize(const NSSize &size)