Vuo  2.3.2
VuoGraphicsWindow.m
Go to the documentation of this file.
1 
10 #import "module.h"
11 #import "VuoApp.h"
12 #import "VuoGraphicsLayer.h"
13 #import "VuoGraphicsView.h"
14 #import "VuoGraphicsWindow.h"
17 #import "VuoScreenCommon.h"
18 #import <Carbon/Carbon.h>
19 
20 #ifdef VUO_COMPILER
22  "title" : "VuoGraphicsWindow",
23  "dependencies" : [
24  "VuoGraphicsLayer",
25  "VuoGraphicsView",
26  "VuoGraphicsWindowDelegate",
27  "VuoRenderedLayers",
28  "VuoScreenCommon",
29  "VuoWindowProperty",
30  "VuoWindowRecorder",
31  "VuoWindowReference",
32  "VuoList_VuoWindowProperty"
33  ]
34 });
35 #endif
36 
40 
41 const NSInteger VuoViewMenuItemTag = 1000;
42 const NSInteger VuoFullScreenMenuItemTag = 1001;
43 
46 
50 static void __attribute__((constructor)) VuoGraphicsWindow_init()
51 {
52  VuoGraphicsWindow_fullScreenTransitionSemaphore = dispatch_semaphore_create(1);
53 }
54 
55 
59 @interface VuoGraphicsWindow ()
60 @property(retain) NSMenuItem *recordMenuItem;
61 @property(retain) id<NSWindowDelegate> privateDelegate;
62 @property bool constrainToScreen;
63 @property(retain) NSScreen *shouldGoFullscreen;
64 
65 @property bool leftMouseDown;
66 @property id mouseMonitor;
67 @property NSSize pendingResize;
68 @end
69 
70 @implementation VuoGraphicsWindow
71 
75 - (instancetype)initWithInitCallback:(VuoGraphicsWindowInitCallback)initCallback
76  updateBackingCallback:(VuoGraphicsWindowUpdateBackingCallback)updateBackingCallback
77  resizeCallback:(VuoGraphicsWindowResizeCallback)resizeCallback
78  drawCallback:(VuoGraphicsWindowDrawCallback)drawCallback
79  userData:(void *)userData
80 {
81  NSRect mainScreenFrame = [[NSScreen mainScreen] frame];
82  _contentRectWhenWindowed = NSMakeRect(mainScreenFrame.origin.x, mainScreenFrame.origin.y, VuoGraphicsWindowDefaultWidth, 768);
83  _styleMaskWhenWindowed = NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
84  if (self = [super initWithContentRect:_contentRectWhenWindowed
85  styleMask:_styleMaskWhenWindowed
86  backing:NSBackingStoreBuffered
87  defer:NO])
88  {
89  self.colorSpace = NSColorSpace.sRGBColorSpace;
90 
91  self.privateDelegate = [[[VuoGraphicsWindowDelegate alloc] initWithWindow:self] autorelease];
92  self.delegate = self.privateDelegate;
93  self.releasedWhenClosed = NO;
94 
95  _cursor = VuoCursor_Pointer;
96 
97  self.contentMinSize = NSMakeSize(VuoGraphicsWindowMinSize, VuoGraphicsWindowMinSize);
98 
99  self.acceptsMouseMovedEvents = YES;
100 
101  self.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
102 
103  [self initDrag];
104 #pragma clang diagnostic push
105 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
106  // The replacement, NSPasteboardTypeFileURL, isn't available until macOS 11.
107  [self registerForDraggedTypes:@[NSFilenamesPboardType]];
108 #pragma clang diagnostic pop
109 
110  _userResizedWindow = NO;
111  _programmaticallyResizingWindow = NO;
112  _constrainToScreen = YES;
113 
114  char *title = VuoApp_getName();
115  self.title = [NSString stringWithUTF8String:title];
116  free(title);
117 
118  _backingScaleFactorCached = self.backingScaleFactor;
119 
120  VuoGraphicsLayer *l = [[VuoGraphicsLayer alloc] initWithWindow:self
121  initCallback:initCallback
122  updateBackingCallback:updateBackingCallback
123  backingScaleFactor:_backingScaleFactorCached
124  resizeCallback:resizeCallback
125  drawCallback:drawCallback
126  userData:userData];
127  l.contentsScale = _backingScaleFactorCached;
128 
129  VuoGraphicsView *v = [[VuoGraphicsView alloc] init];
130  v.layer = l;
131  [l release];
132 
133  self.contentView = v;
134  [v release];
135 
136  _contentRectCached = l.frame;
137 
138  _mouseMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown|NSEventMaskLeftMouseUp handler:^(NSEvent *event) {
139  if (event.type == NSEventTypeLeftMouseDown)
140  _leftMouseDown = true;
141  else if (event.type == NSEventTypeLeftMouseUp)
142  {
143  _leftMouseDown = false;
144  if (!NSEqualSizes(_pendingResize, NSZeroSize))
145  {
146  VDebugLog("The window drag has concluded; performing the deferred resize.");
147  self.contentSize = _pendingResize;
148  _pendingResize = NSZeroSize;
149  }
150  }
151  return event;
152  }];
153  }
154 
155  return self;
156 }
157 
164 - (void)draw
165 {
166  __block VuoGraphicsLayer *l;
168  NSView *v = self.contentView;
169  l = (VuoGraphicsLayer *)v.layer;
170  });
171  [l draw];
172 }
173 
177 - (BOOL)canBecomeMainWindow
178 {
179  return YES;
180 }
181 
185 - (BOOL)canBecomeKeyWindow
186 {
187  return YES;
188 }
189 
190 - (void)updateFullScreenMenu
191 {
192  NSMenuItem *fullScreenMenuItem = [[[[[NSApplication sharedApplication] mainMenu] itemWithTag:VuoViewMenuItemTag] submenu] itemWithTag:VuoFullScreenMenuItemTag];
193  fullScreenMenuItem.title = self.isFullScreen ? @"Exit Full Screen" : @"Enter Full Screen";
194 }
195 
201 - (void)becomeMainWindow
202 {
203  [super becomeMainWindow];
204 
205  if (!_isInMacFullScreenMode)
206  [self setFullScreenPresentation:self.isFullScreen];
207 
208  NSMenu *fileMenu = [[[NSMenu alloc] initWithTitle:@"File"] autorelease];
209  _recordMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleRecording) keyEquivalent:@"e"];
210  [_recordMenuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand|NSEventModifierFlagOption];
211  [fileMenu addItem:_recordMenuItem];
212  NSMenuItem *fileMenuItem = [[NSMenuItem new] autorelease];
213  [fileMenuItem setSubmenu:fileMenu];
214 
215  NSMenu *viewMenu = [[[NSMenu alloc] initWithTitle:@"View"] autorelease];
216  NSMenuItem *fullScreenMenuItem = [[[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleFullScreen) keyEquivalent:@"f"] autorelease];
217  fullScreenMenuItem.tag = VuoFullScreenMenuItemTag;
218  [viewMenu addItem:fullScreenMenuItem];
219  NSMenuItem *viewMenuItem = [[NSMenuItem new] autorelease];
220  viewMenuItem.tag = VuoViewMenuItemTag;
221  [viewMenuItem setSubmenu:viewMenu];
222 
223  NSMenu *windowMenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
224  NSMenuItem *minimizeMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease];
225  NSMenuItem *zoomMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""] autorelease];
226  NSMenuItem *cycleMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Cycle Through Windows" action:@selector(_cycleWindows:) keyEquivalent:@"`"] autorelease];
227  [windowMenu addItem:minimizeMenuItem];
228  [windowMenu addItem:zoomMenuItem];
229  [windowMenu addItem:cycleMenuItem];
230  NSMenuItem *windowMenuItem = [[NSMenuItem new] autorelease];
231  [windowMenuItem setSubmenu:windowMenu];
232 
233  NSMutableArray *windowMenuItems = [NSMutableArray arrayWithCapacity:3];
234  [windowMenuItems addObject:fileMenuItem];
235  [windowMenuItems addObject:viewMenuItem];
236  [windowMenuItems addObject:windowMenuItem];
237  self.oldMenu = (NSMenu *)VuoApp_setMenuItems(windowMenuItems);
238 
239  [self updateFullScreenMenu];
240 
241  [self updateUI];
242 }
243 
247 - (void)resignMainWindow
248 {
249  [super resignMainWindow];
250 
251  // No need to change presentation when resigning, since:
252  // - other VuoGraphicsWindows change presentation on becomeMainWindow
253  // - Mac OS X changes presentation when another app becomes active
254  //
255  // Also, if two VuoGraphicsWindows are fullscreen on separate displays,
256  // switching out of fullscreen presentation will cause the menubar and dock to flicker
257  // when clicking from one display to another.
258 // if (!_isInMacFullScreenMode)
259 // [self setFullScreenPresentation:NO];
260 
261  VuoApp_setMenu(_oldMenu);
262  self.oldMenu = nil;
263 }
264 
268 - (void)keyDown:(NSEvent *)event
269 {
270  if ([event keyCode] == kVK_Escape && [self isFullScreen])
271  [super keyDown:event];
272 }
273 
277 - (void)updateUI
278 {
279  if (_recorder)
280  _recordMenuItem.title = @"Stop Recording…";
281  else
282  _recordMenuItem.title = @"Start Recording";
283 }
284 
290 - (void)enableUpdatedWindowTrigger:(VuoGraphicsWindowUpdatedWindowCallback)updatedWindow
291 {
292  _updatedWindow = updatedWindow;
293 
296  _updatedWindow(rl);
297 
298  __block VuoGraphicsLayer *l;
300  NSView *v = self.contentView;
301  l = (VuoGraphicsLayer *)v.layer;
302  });
303  [l enableTriggers];
304 }
305 
312 - (void)enableShowedWindowTrigger:(VuoGraphicsWindowShowedWindowCallback)showedWindow requestedFrameTrigger:(VuoGraphicsWindowRequestedFrameCallback)requestedFrame
313 {
314  _showedWindow = showedWindow;
315  _showedWindow(VuoWindowReference_make(self));
316 
317  _requestedFrame = requestedFrame;
318 
319  __block VuoGraphicsLayer *l;
321  NSView *v = self.contentView;
322  l = (VuoGraphicsLayer *)v.layer;
323  });
324  [l enableTriggers];
325 }
326 
332 - (void)disableTriggers
333 {
334  __block VuoGraphicsLayer *l;
336  NSView *v = self.contentView;
337  l = (VuoGraphicsLayer *)v.layer;
338  });
339  [l disableTriggers];
340 
341  _updatedWindow = NULL;
342  _showedWindow = NULL;
343  _requestedFrame = NULL;
344 }
345 
351 - (bool)isFullScreen
352 {
353  __block NSUInteger styleMask;
355  styleMask = self.styleMask;
356  });
357  return _isInMacFullScreenMode || styleMask == 0;
358 }
359 
360 
364 - (void)setTitle:(NSString *)title
365 {
366  self.titleBackup = title;
367  super.title = title;
368 }
369 
375 - (void)setProperties:(VuoList_VuoWindowProperty)properties
376 {
377  unsigned int propertyCount = VuoListGetCount_VuoWindowProperty(properties);
378  for (unsigned int i = 1; i <= propertyCount; ++i)
379  {
380  VuoWindowProperty property = VuoListGetValue_VuoWindowProperty(properties, i);
381  VDebugLog("%s", VuoWindowProperty_getSummary(property));
382 
383  if (property.type == VuoWindowProperty_Title)
384  {
385  self.title = property.title ? [NSString stringWithUTF8String:property.title] : @"";
386  if (_updatedWindow)
387  {
390  _updatedWindow(rl);
391  }
392  }
393  else if (property.type == VuoWindowProperty_FullScreen)
394  {
395  NSScreen *requestedScreen = VuoScreen_getNSScreen(property.screen);
396 
397  bool isFullScreen = self.isFullScreen;
398  bool wantsFullScreen = property.fullScreen;
399 
400  // Only go fullscreen if the specific requested screen exists.
401  // https://b33p.net/kosada/node/14658
402  if (wantsFullScreen && !requestedScreen)
403  continue;
404 
405  NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
406  NSInteger currentDeviceId = [[[self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
407  bool changingScreen = requestedDeviceId != currentDeviceId;
408 
409  if (isFullScreen && wantsFullScreen && changingScreen)
410  {
411  // Temporarily switch back to windowed mode so that we can switch to fullscreen on the new screen
412  // (since macOS doesn't let us directly move from one fullscreen to another).
413  [self setFullScreen:NO onScreen:nil];
414 
415  // If `System Preferences > Mission Control > Displays have separate Spaces` is enabled…
416  if (NSScreen.screensHaveSeparateSpaces)
417  // Give macOS a chance to complete its exit-fullscreen transition.
418  self.shouldGoFullscreen = requestedScreen;
419  else
420  // If not, we can immediately go fullscreen on the new screen.
421  [self setFullScreen:YES onScreen:requestedScreen];
422  }
423  else if (isFullScreen != wantsFullScreen)
424  [self setFullScreen:property.fullScreen onScreen:requestedScreen];
425  else if (requestedScreen && !isFullScreen)
426  // Move the non-fullscreen window to the center of the specified screen.
427  [self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
428  NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
429  }
430  else if (property.type == VuoWindowProperty_Position)
431  {
432  NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
433  if (property.unit == VuoCoordinateUnit_Pixels)
434  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
435 
436  NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
437  if ([self isFullScreen])
438  _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
439  else
440  {
441  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
442  self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
443  }
444  }
445  else if (property.type == VuoWindowProperty_Size)
446  {
447  NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
448  if (property.unit == VuoCoordinateUnit_Pixels)
449  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
450  _maintainsPixelSizeWhenBackingChanges = (property.unit == VuoCoordinateUnit_Pixels);
451 
452  if ([self isFullScreen])
453  _contentRectWhenWindowed.size = propertyInPoints.size;
454  else
455  {
456  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
457 
458  // Adjust the y position by the change in height, so that the window appears to be anchored in its top-left corner
459  // (instead of its bottom-left corner as the system does by default).
460  contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
461 
462  contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
463  @try
464  {
465  _constrainToScreen = NO;
466  [self setFrame:[self frameRectForContentRect:contentRect] display:YES animate:NO];
467  _constrainToScreen = YES;
468  }
469  @catch (NSException *e)
470  {
471  VuoText description = VuoText_makeFromCFString(e.description);
472  VuoLocal(description);
473  VUserLog("Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
474  }
475  }
476  }
477  else if (property.type == VuoWindowProperty_AspectRatio)
478  {
479  if (property.aspectRatio < 1./10000
480  || property.aspectRatio > 10000)
481  {
482  VUserLog("Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
483  continue;
484  }
485 
486  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
487  [self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
488  }
489  else if (property.type == VuoWindowProperty_AspectRatioReset)
490  [self unlockAspectRatio];
491  else if (property.type == VuoWindowProperty_Resizable)
492  {
493  [[self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
494  if ([self isFullScreen])
495  _styleMaskWhenWindowed = property.resizable ? (_styleMaskWhenWindowed | NSWindowStyleMaskResizable) : (_styleMaskWhenWindowed & ~NSWindowStyleMaskResizable);
496  else
497  self.styleMask = property.resizable ? ([self styleMask] | NSWindowStyleMaskResizable) : ([self styleMask] & ~NSWindowStyleMaskResizable);
498  }
499  else if (property.type == VuoWindowProperty_Cursor)
500  {
501  _cursor = property.cursor;
502  [self invalidateCursorRectsForView:self.contentView];
503  }
504  else if (property.type == VuoWindowProperty_Level)
505  {
506  if (property.level == VuoWindowLevel_Background)
507  self.level = CGWindowLevelForKey(kCGBackstopMenuLevelKey);
508  else if (property.level == VuoWindowLevel_Normal)
509  self.level = CGWindowLevelForKey(kCGNormalWindowLevelKey);
510  else if (property.level == VuoWindowLevel_Floating)
511  self.level = CGWindowLevelForKey(kCGFloatingWindowLevelKey);
512  }
513  }
514 }
515 
522 - (NSRect)liveFrame
523 {
524  Rect qdRect;
525  extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
526 
527  if (GetWindowBounds(self.windowRef, kWindowStructureRgn, &qdRect) == noErr)
528  return NSMakeRect(qdRect.left,
529  (float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
530  qdRect.right - qdRect.left,
531  qdRect.bottom - qdRect.top);
532  else
533  // The above GetWindowBounds API seems to always return `paramErr` on macOS 11.
534  return self.frame;
535 }
536 
547 - (void)setAspectRatioToWidth:(unsigned int)pixelsWide height:(unsigned int)pixelsHigh
548 {
549  pixelsWide = MAX(VuoGraphicsWindowMinSize, pixelsWide);
550  pixelsHigh = MAX(VuoGraphicsWindowMinSize, pixelsHigh);
551 
552  NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
553  if (NSEqualSizes([self contentAspectRatio], newAspect))
554  return;
555 
556  // Sets the constraint when the user resizes the window (but doesn't affect the window's current size).
557  self.contentAspectRatio = newAspect;
558 
559  if ([self isFullScreen])
560  {
561  if (_updatedWindow)
562  {
565  _updatedWindow(rl);
566  }
567  if (_showedWindow)
568  _showedWindow(VuoWindowReference_make(self));
569  return;
570  }
571 
572  CGFloat desiredWidth = pixelsWide;
573  CGFloat desiredHeight = pixelsHigh;
574  CGRect windowFrame = [self liveFrame];
575  CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
576 
577  // Adjust the width and height if the user manually resized the window.
578  if (_userResizedWindow)
579  {
580  // Preserve the width, scale the height.
581  desiredWidth = CGRectGetWidth(windowFrame);
582  desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
583  }
584 
585  // Adjust the width and height if they don't fit the screen.
586  NSRect screenFrame = [[self screen] visibleFrame];
587  NSRect maxContentRect = [self contentRectForFrameRect:screenFrame];
588  if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
589  {
590  CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
591  if (aspectRatio >= maxContentAspectRatio)
592  {
593  // Too wide, so scale it to the maximum horizontal screen space.
594  desiredWidth = maxContentRect.size.width;
595  desiredHeight = maxContentRect.size.width / aspectRatio;
596  }
597  else
598  {
599  // Too tall, so scale it to the maximum vertical screen space.
600  desiredWidth = maxContentRect.size.height * aspectRatio;
601  desiredHeight = maxContentRect.size.height;
602  }
603  }
604 
605  NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
606  NSSize newWindowSize = [self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
607 
608  // Preserve the window's top left corner position. (If you just resize, it preserves the bottom left corner position.)
609  CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
610  NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
611 
612  _programmaticallyResizingWindow = YES;
613  [self setFrame:newWindowFrame display:YES];
614  _programmaticallyResizingWindow = NO;
615 
616  if (_updatedWindow)
617  {
620  _updatedWindow(rl);
621  }
622  if (_showedWindow)
623  _showedWindow(VuoWindowReference_make(self));
624 }
625 
631 - (void)unlockAspectRatio
632 {
633  self.resizeIncrements = NSMakeSize(1,1);
634 }
635 
640 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
641 {
642  if (_constrainToScreen)
643  return [super constrainFrameRect:frameRect toScreen:screen];
644  else
645  return frameRect;
646 }
647 
651 - (void)setFullScreenPresentation:(bool)enabled
652 {
653  if (enabled)
654  {
655  // Don't raise the window level, since the window still covers the screen even when it's not focused.
656 // self.level = NSScreenSaverWindowLevel;
657 
658  // Instead, just hide the dock and menu bar.
659  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
660  }
661  else
662  {
663 // self.level = NSNormalWindowLevel;
664  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
665  }
666 }
667 
704 - (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
705 {
706  _programmaticallyResizingWindow = true;
707 
708  if (wantsFullScreen && ![self isFullScreen])
709  {
710  // Switch into fullscreen mode.
711 
712 
713  // Save the position, size, and style, to be restored when switching back to windowed mode.
714  _contentRectWhenWindowed = [self contentRectForFrameRect:self.frame];
715  _styleMaskWhenWindowed = [self styleMask];
716 
717  // Move the window to the specified screen.
718  // (Necessary even for Mac-fullscreen mode, since it fullscreens on whichever screen the window is currently on.)
719  if (screen && ![[self screen] isEqualTo:screen])
720  {
721  NSRect windowFrame = self.frame;
722  NSRect screenFrame = screen.frame;
723  self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
724  screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
725  }
726 
727 
728  // Use Mac-fullscreen mode by default, since it's compatible with Mission Control,
729  // and since (unlike Kiosk Mode) the menu bar stays hidden on the fullscreen display
730  // when you focus a window on a different display (such as when livecoding during a performance).
731  bool useMacFullScreenMode = true;
732 
733  // If `System Preferences > Mission Control > Displays have separate Spaces` is unchecked,
734  // only a single window could go Mac-fullscreen at once, so don't use Mac-fullscreen mode in that case.
735  if (!NSScreen.screensHaveSeparateSpaces)
736  useMacFullScreenMode = false;
737 
738 
739  if (useMacFullScreenMode)
740  {
741  _isInMacFullScreenMode = YES;
742 
743  // If a size-locked window enters Mac-fullscreen mode,
744  // its height increases upon exiting Mac-fullscreen mode.
745  // Mark it resizeable while fullscreen, to keep its size from changing (!).
746  self.styleMask |= NSWindowStyleMaskResizable;
747 
748  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
749  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
750  dispatch_async(dispatch_get_main_queue(), ^{
751  _programmaticallyTransitioningFullScreen = true;
752  [self toggleFullScreen:nil];
753  [self updateFullScreenMenu];
754  });
755  });
756  }
757  else
758  {
759  [self setFullScreenPresentation:YES];
760 
761  NSSize car = self.contentAspectRatio;
762  self.styleMask = 0;
763  if (!NSEqualSizes(car, NSMakeSize(0,0)))
764  self.contentAspectRatio = car;
765 
766  // Make the window take up the whole screen.
767  [self setFrame:self.screen.frame display:YES];
768 
769  [self finishFullScreenTransition];
770  [self updateFullScreenMenu];
771  }
772  }
773  else if (!wantsFullScreen && [self isFullScreen])
774  {
775  // Switch out of fullscreen mode.
776 
777  if (_isInMacFullScreenMode)
778  {
779  _isInMacFullScreenMode = NO;
780 
781  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
782  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
783  dispatch_async(dispatch_get_main_queue(), ^{
784  _programmaticallyTransitioningFullScreen = true;
785  [self toggleFullScreen:nil];
786  self.styleMask = _styleMaskWhenWindowed;
787  [self updateFullScreenMenu];
788  });
789  });
790  }
791  else
792  {
793  [self setFullScreenPresentation:NO];
794 
795  NSSize car = self.contentAspectRatio;
796  self.styleMask = _styleMaskWhenWindowed;
797  self.title = _titleBackup;
798  if (!NSEqualSizes(car, NSMakeSize(0,0)))
799  self.contentAspectRatio = car;
800 
801  [self setFrame:[self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
802 
803  [self finishFullScreenTransition];
804  [self updateFullScreenMenu];
805  }
806  }
807 }
808 
815 {
816  _programmaticallyResizingWindow = false;
817 
818  NSNotification *n = [NSNotification notificationWithName:@"unused" object:nil];
819  [self.delegate windowDidResize:n];
820 
821  [self makeFirstResponder:self];
822 
823  if (self.shouldGoFullscreen)
824  dispatch_async(dispatch_get_main_queue(), ^{
825  NSScreen *s = [self.shouldGoFullscreen retain];
826  self.shouldGoFullscreen = nil;
827  [self setFullScreen:true onScreen:s];
828  [s release];
829  });
830 }
831 
837 - (void)toggleFullScreen
838 {
839  [self setFullScreen:![self isFullScreen] onScreen:nil];
840 }
841 
846 - (void)scheduleResize:(NSSize)newSize
847 {
848  VDebugLog("Requested to resize to %gx%g points.",newSize.width,newSize.height);
849  if (_leftMouseDown)
850  {
851  VDebugLog(" The window is being dragged; deferring the resize.");
852  _pendingResize = newSize;
853  }
854  else
855  {
856  VDebugLog(" The window is not being dragged; resizing now.");
857  self.contentSize = newSize;
858  }
859 }
860 
866 - (void)toggleRecording
867 {
868  if (!self.recorder)
869  {
870  // Start recording to a temporary file (rather than first asking for the filename, to make it quicker to start recording).
871  self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
872  _recorder = [[VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
873 
874  [self updateUI];
875  }
876  else
877  [self stopRecording];
878 }
879 
883 - (void)promptToSaveMovie
884 {
885  NSSavePanel *sp = [NSSavePanel savePanel];
886  [sp setTitle:@"Save Movie"];
887  [sp setNameFieldLabel:@"Save Movie To:"];
888  [sp setPrompt:@"Save"];
889 #pragma clang diagnostic push
890 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
891  // The replacement, setAllowedContentTypes, isn't available until macOS 11.
892  [sp setAllowedFileTypes:@[@"mov"]];
893 #pragma clang diagnostic pop
894 
895  char *title = VuoApp_getName();
896  sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
897  free(title);
898 
899  if ([sp runModal] == NSModalResponseCancel)
900  goto done;
901 
902  NSError *error;
903  if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
904  {
905  if ([error code] == NSFileWriteFileExistsError)
906  {
907  // File exists. Since, in the NSSavePanel, the user said to Replace, try replacing it.
908  if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
909  withItemAtURL:_temporaryMovieURL
910  backupItemName:nil
911  options:0
912  resultingItemURL:nil
913  error:&error])
914  {
915  // Replacement failed; show error…
916  NSAlert *alert = [NSAlert alertWithError:error];
917  [alert runModal];
918 
919  // …and give the user another chance.
920  [self promptToSaveMovie];
921  }
922  goto done;
923  }
924 
925  NSAlert *alert = [NSAlert alertWithError:error];
926  [alert runModal];
927  }
928 
929 done:
930  self.temporaryMovieURL = nil;
931 }
932 
940 - (void)stopRecording
941 {
942  if (!_recorder)
943  return;
944 
945  [_recorder finish];
946  [_recorder release];
947  _recorder = nil;
948 
949  [self updateUI];
950 
951  [self setFullScreen:NO onScreen:nil];
952 
953  [self promptToSaveMovie];
954 }
955 
961 - (void)cancelOperation:(id)sender
962 {
963  [self setFullScreen:NO onScreen:nil];
964 }
965 
969 - (void)close
970 {
971  NSView *v = self.contentView;
972  VuoGraphicsLayer *gv = (VuoGraphicsLayer *)v.layer;
973  [gv close];
974 
975  [super close];
976 }
977 
981 - (void)dealloc
982 {
983  [NSEvent removeMonitor:_mouseMonitor];
984 
985  // https://b33p.net/kosada/node/12123
986  // Cocoa leaks the contentView unless we force it to resign firstResponder status.
987  [self makeFirstResponder:nil];
988 
989  [super dealloc];
990 }
991 
992 @end