Vuo  2.1.1
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 = NSTitledWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask;
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  [self registerForDraggedTypes:@[NSFilenamesPboardType]];
105 
106  _userResizedWindow = NO;
107  _programmaticallyResizingWindow = NO;
108  _constrainToScreen = YES;
109 
110  char *title = VuoApp_getName();
111  self.title = [NSString stringWithUTF8String:title];
112  free(title);
113 
114  _backingScaleFactorCached = self.backingScaleFactor;
115 
116  VuoGraphicsLayer *l = [[VuoGraphicsLayer alloc] initWithWindow:self
117  initCallback:initCallback
118  updateBackingCallback:updateBackingCallback
119  backingScaleFactor:_backingScaleFactorCached
120  resizeCallback:resizeCallback
121  drawCallback:drawCallback
122  userData:userData];
123  l.contentsScale = _backingScaleFactorCached;
124 
125  VuoGraphicsView *v = [[VuoGraphicsView alloc] init];
126  v.layer = l;
127  [l release];
128 
129  self.contentView = v;
130  [v release];
131 
132  _contentRectCached = l.frame;
133 
134  _mouseMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSLeftMouseUpMask handler:^(NSEvent *event) {
135  if (event.type == NSLeftMouseDown)
136  _leftMouseDown = true;
137  else if (event.type == NSLeftMouseUp)
138  {
139  _leftMouseDown = false;
140  if (!NSEqualSizes(_pendingResize, NSZeroSize))
141  {
142  VDebugLog("The window drag has concluded; performing the deferred resize.");
143  self.contentSize = _pendingResize;
144  _pendingResize = NSZeroSize;
145  }
146  }
147  return event;
148  }];
149  }
150 
151  return self;
152 }
153 
160 - (void)draw
161 {
162  __block VuoGraphicsLayer *l;
164  NSView *v = self.contentView;
165  l = (VuoGraphicsLayer *)v.layer;
166  });
167  [l draw];
168 }
169 
173 - (BOOL)canBecomeMainWindow
174 {
175  return YES;
176 }
177 
181 - (BOOL)canBecomeKeyWindow
182 {
183  return YES;
184 }
185 
186 - (void)updateFullScreenMenu
187 {
188  NSMenuItem *fullScreenMenuItem = [[[[[NSApplication sharedApplication] mainMenu] itemWithTag:VuoViewMenuItemTag] submenu] itemWithTag:VuoFullScreenMenuItemTag];
189  fullScreenMenuItem.title = self.isFullScreen ? @"Exit Full Screen" : @"Enter Full Screen";
190 }
191 
197 - (void)becomeMainWindow
198 {
199  [super becomeMainWindow];
200 
201  if (!_isInMacFullScreenMode)
202  [self setFullScreenPresentation:self.isFullScreen];
203 
204  NSMenu *fileMenu = [[[NSMenu alloc] initWithTitle:@"File"] autorelease];
205  _recordMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleRecording) keyEquivalent:@"e"];
206  [_recordMenuItem setKeyEquivalentModifierMask:NSCommandKeyMask|NSAlternateKeyMask];
207  [fileMenu addItem:_recordMenuItem];
208  NSMenuItem *fileMenuItem = [[NSMenuItem new] autorelease];
209  [fileMenuItem setSubmenu:fileMenu];
210 
211  NSMenu *viewMenu = [[[NSMenu alloc] initWithTitle:@"View"] autorelease];
212  NSMenuItem *fullScreenMenuItem = [[[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleFullScreen) keyEquivalent:@"f"] autorelease];
213  fullScreenMenuItem.tag = VuoFullScreenMenuItemTag;
214  [viewMenu addItem:fullScreenMenuItem];
215  NSMenuItem *viewMenuItem = [[NSMenuItem new] autorelease];
216  viewMenuItem.tag = VuoViewMenuItemTag;
217  [viewMenuItem setSubmenu:viewMenu];
218 
219  NSMenu *windowMenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
220  NSMenuItem *minimizeMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease];
221  NSMenuItem *zoomMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""] autorelease];
222  NSMenuItem *cycleMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Cycle Through Windows" action:@selector(_cycleWindows:) keyEquivalent:@"`"] autorelease];
223  [windowMenu addItem:minimizeMenuItem];
224  [windowMenu addItem:zoomMenuItem];
225  [windowMenu addItem:cycleMenuItem];
226  NSMenuItem *windowMenuItem = [[NSMenuItem new] autorelease];
227  [windowMenuItem setSubmenu:windowMenu];
228 
229  NSMutableArray *windowMenuItems = [NSMutableArray arrayWithCapacity:3];
230  [windowMenuItems addObject:fileMenuItem];
231  [windowMenuItems addObject:viewMenuItem];
232  [windowMenuItems addObject:windowMenuItem];
233  self.oldMenu = (NSMenu *)VuoApp_setMenuItems(windowMenuItems);
234 
235  [self updateFullScreenMenu];
236 
237  [self updateUI];
238 }
239 
243 - (void)resignMainWindow
244 {
245  [super resignMainWindow];
246 
247  // No need to change presentation when resigning, since:
248  // - other VuoGraphicsWindows change presentation on becomeMainWindow
249  // - Mac OS X changes presentation when another app becomes active
250  //
251  // Also, if two VuoGraphicsWindows are fullscreen on separate displays,
252  // switching out of fullscreen presentation will cause the menubar and dock to flicker
253  // when clicking from one display to another.
254 // if (!_isInMacFullScreenMode)
255 // [self setFullScreenPresentation:NO];
256 
257  VuoApp_setMenu(_oldMenu);
258  self.oldMenu = nil;
259 }
260 
264 - (void)keyDown:(NSEvent *)event
265 {
266  if ([event keyCode] == kVK_Escape && [self isFullScreen])
267  [super keyDown:event];
268 }
269 
273 - (void)updateUI
274 {
275  if (_recorder)
276  _recordMenuItem.title = @"Stop Recording…";
277  else
278  _recordMenuItem.title = @"Start Recording";
279 }
280 
286 - (void)enableUpdatedWindowTrigger:(VuoGraphicsWindowUpdatedWindowCallback)updatedWindow
287 {
288  _updatedWindow = updatedWindow;
289 
292  _updatedWindow(rl);
293 
294  __block VuoGraphicsLayer *l;
296  NSView *v = self.contentView;
297  l = (VuoGraphicsLayer *)v.layer;
298  });
299  [l enableTriggers];
300 }
301 
308 - (void)enableShowedWindowTrigger:(VuoGraphicsWindowShowedWindowCallback)showedWindow requestedFrameTrigger:(VuoGraphicsWindowRequestedFrameCallback)requestedFrame
309 {
310  _showedWindow = showedWindow;
311  _showedWindow(VuoWindowReference_make(self));
312 
313  _requestedFrame = requestedFrame;
314 
315  __block VuoGraphicsLayer *l;
317  NSView *v = self.contentView;
318  l = (VuoGraphicsLayer *)v.layer;
319  });
320  [l enableTriggers];
321 }
322 
328 - (void)disableTriggers
329 {
330  __block VuoGraphicsLayer *l;
332  NSView *v = self.contentView;
333  l = (VuoGraphicsLayer *)v.layer;
334  });
335  [l disableTriggers];
336 
337  _updatedWindow = NULL;
338  _showedWindow = NULL;
339  _requestedFrame = NULL;
340 }
341 
347 - (bool)isFullScreen
348 {
349  __block NSUInteger styleMask;
351  styleMask = self.styleMask;
352  });
353  return _isInMacFullScreenMode || styleMask == 0;
354 }
355 
356 
360 - (void)setTitle:(NSString *)title
361 {
362  self.titleBackup = title;
363  super.title = title;
364 }
365 
371 - (void)setProperties:(VuoList_VuoWindowProperty)properties
372 {
373  unsigned int propertyCount = VuoListGetCount_VuoWindowProperty(properties);
374  for (unsigned int i = 1; i <= propertyCount; ++i)
375  {
376  VuoWindowProperty property = VuoListGetValue_VuoWindowProperty(properties, i);
377  VDebugLog("%s", VuoWindowProperty_getSummary(property));
378 
379  if (property.type == VuoWindowProperty_Title)
380  {
381  self.title = property.title ? [NSString stringWithUTF8String:property.title] : @"";
382  if (_updatedWindow)
383  {
386  _updatedWindow(rl);
387  }
388  }
389  else if (property.type == VuoWindowProperty_FullScreen)
390  {
391  NSScreen *requestedScreen = VuoScreen_getNSScreen(property.screen);
392 
393  bool isFullScreen = self.isFullScreen;
394  bool wantsFullScreen = property.fullScreen;
395 
396  // Only go fullscreen if the specific requested screen exists.
397  // https://b33p.net/kosada/node/14658
398  if (wantsFullScreen && !requestedScreen)
399  continue;
400 
401  NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
402  NSInteger currentDeviceId = [[[self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
403  bool changingScreen = requestedDeviceId != currentDeviceId;
404 
405  if (isFullScreen && wantsFullScreen && changingScreen)
406  {
407  // Temporarily switch back to windowed mode so that we can switch to fullscreen on the new screen
408  // (since macOS doesn't let us directly move from one fullscreen to another).
409  [self setFullScreen:NO onScreen:nil];
410 
411  // If `System Preferences > Mission Control > Displays have separate Spaces` is enabled…
412  if (NSScreen.screensHaveSeparateSpaces)
413  // Give macOS a chance to complete its exit-fullscreen transition.
414  self.shouldGoFullscreen = requestedScreen;
415  else
416  // If not, we can immediately go fullscreen on the new screen.
417  [self setFullScreen:YES onScreen:requestedScreen];
418  }
419  else if (isFullScreen != wantsFullScreen)
420  [self setFullScreen:property.fullScreen onScreen:requestedScreen];
421  else if (requestedScreen && !isFullScreen)
422  // Move the non-fullscreen window to the center of the specified screen.
423  [self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
424  NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
425  }
426  else if (property.type == VuoWindowProperty_Position)
427  {
428  NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
429  if (property.unit == VuoCoordinateUnit_Pixels)
430  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
431 
432  NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
433  if ([self isFullScreen])
434  _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
435  else
436  {
437  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
438  self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
439  }
440  }
441  else if (property.type == VuoWindowProperty_Size)
442  {
443  NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
444  if (property.unit == VuoCoordinateUnit_Pixels)
445  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
446  _maintainsPixelSizeWhenBackingChanges = (property.unit == VuoCoordinateUnit_Pixels);
447 
448  if ([self isFullScreen])
449  _contentRectWhenWindowed.size = propertyInPoints.size;
450  else
451  {
452  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
453 
454  // Adjust the y position by the change in height, so that the window appears to be anchored in its top-left corner
455  // (instead of its bottom-left corner as the system does by default).
456  contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
457 
458  contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
459  @try
460  {
461  _constrainToScreen = NO;
462  [self setFrame:[self frameRectForContentRect:contentRect] display:YES animate:NO];
463  _constrainToScreen = YES;
464  }
465  @catch (NSException *e)
466  {
467  VuoText description = VuoText_makeFromCFString(e.description);
468  VuoLocal(description);
469  VUserLog("Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
470  }
471  }
472  }
473  else if (property.type == VuoWindowProperty_AspectRatio)
474  {
475  if (property.aspectRatio < 1./10000
476  || property.aspectRatio > 10000)
477  {
478  VUserLog("Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
479  continue;
480  }
481 
482  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
483  [self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
484  }
485  else if (property.type == VuoWindowProperty_AspectRatioReset)
486  [self unlockAspectRatio];
487  else if (property.type == VuoWindowProperty_Resizable)
488  {
489  [[self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
490  if ([self isFullScreen])
491  _styleMaskWhenWindowed = property.resizable ? (_styleMaskWhenWindowed | NSResizableWindowMask) : (_styleMaskWhenWindowed & ~NSResizableWindowMask);
492  else
493  self.styleMask = property.resizable ? ([self styleMask] | NSResizableWindowMask) : ([self styleMask] & ~NSResizableWindowMask);
494  }
495  else if (property.type == VuoWindowProperty_Cursor)
496  {
497  _cursor = property.cursor;
498  [self invalidateCursorRectsForView:self.contentView];
499  }
500  else if (property.type == VuoWindowProperty_Level)
501  {
502  if (property.level == VuoWindowLevel_Background)
503  self.level = CGWindowLevelForKey(kCGBackstopMenuLevelKey);
504  else if (property.level == VuoWindowLevel_Normal)
505  self.level = CGWindowLevelForKey(kCGNormalWindowLevelKey);
506  else if (property.level == VuoWindowLevel_Floating)
507  self.level = CGWindowLevelForKey(kCGFloatingWindowLevelKey);
508  }
509  }
510 }
511 
518 - (NSRect)liveFrame
519 {
520  Rect qdRect;
521  extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
522 
523  GetWindowBounds([self windowRef], kWindowStructureRgn, &qdRect);
524 
525  return NSMakeRect(qdRect.left,
526  (float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
527  qdRect.right - qdRect.left,
528  qdRect.bottom - qdRect.top);
529 }
530 
541 - (void)setAspectRatioToWidth:(unsigned int)pixelsWide height:(unsigned int)pixelsHigh
542 {
543  pixelsWide = MAX(VuoGraphicsWindowMinSize, pixelsWide);
544  pixelsHigh = MAX(VuoGraphicsWindowMinSize, pixelsHigh);
545 
546  NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
547  if (NSEqualSizes([self contentAspectRatio], newAspect))
548  return;
549 
550  // Sets the constraint when the user resizes the window (but doesn't affect the window's current size).
551  self.contentAspectRatio = newAspect;
552 
553  if ([self isFullScreen])
554  {
555  if (_updatedWindow)
556  {
559  _updatedWindow(rl);
560  }
561  if (_showedWindow)
562  _showedWindow(VuoWindowReference_make(self));
563  return;
564  }
565 
566  CGFloat desiredWidth = pixelsWide;
567  CGFloat desiredHeight = pixelsHigh;
568  CGRect windowFrame = [self liveFrame];
569  CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
570 
571  // Adjust the width and height if the user manually resized the window.
572  if (_userResizedWindow)
573  {
574  // Preserve the width, scale the height.
575  desiredWidth = CGRectGetWidth(windowFrame);
576  desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
577  }
578 
579  // Adjust the width and height if they don't fit the screen.
580  NSRect screenFrame = [[self screen] visibleFrame];
581  NSRect maxContentRect = [self contentRectForFrameRect:screenFrame];
582  if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
583  {
584  CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
585  if (aspectRatio >= maxContentAspectRatio)
586  {
587  // Too wide, so scale it to the maximum horizontal screen space.
588  desiredWidth = maxContentRect.size.width;
589  desiredHeight = maxContentRect.size.width / aspectRatio;
590  }
591  else
592  {
593  // Too tall, so scale it to the maximum vertical screen space.
594  desiredWidth = maxContentRect.size.height * aspectRatio;
595  desiredHeight = maxContentRect.size.height;
596  }
597  }
598 
599  NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
600  NSSize newWindowSize = [self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
601 
602  // Preserve the window's top left corner position. (If you just resize, it preserves the bottom left corner position.)
603  CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
604  NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
605 
606  _programmaticallyResizingWindow = YES;
607  [self setFrame:newWindowFrame display:YES];
608  _programmaticallyResizingWindow = NO;
609 
610  if (_updatedWindow)
611  {
614  _updatedWindow(rl);
615  }
616  if (_showedWindow)
617  _showedWindow(VuoWindowReference_make(self));
618 }
619 
625 - (void)unlockAspectRatio
626 {
627  self.resizeIncrements = NSMakeSize(1,1);
628 }
629 
634 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
635 {
636  if (_constrainToScreen)
637  return [super constrainFrameRect:frameRect toScreen:screen];
638  else
639  return frameRect;
640 }
641 
645 - (void)setFullScreenPresentation:(bool)enabled
646 {
647  if (enabled)
648  {
649  // Don't raise the window level, since the window still covers the screen even when it's not focused.
650 // self.level = NSScreenSaverWindowLevel;
651 
652  // Instead, just hide the dock and menu bar.
653  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
654  }
655  else
656  {
657 // self.level = NSNormalWindowLevel;
658  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
659  }
660 }
661 
698 - (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
699 {
700  _programmaticallyResizingWindow = true;
701 
702  if (wantsFullScreen && ![self isFullScreen])
703  {
704  // Switch into fullscreen mode.
705 
706 
707  // Save the position, size, and style, to be restored when switching back to windowed mode.
708  _contentRectWhenWindowed = [self contentRectForFrameRect:self.frame];
709  _styleMaskWhenWindowed = [self styleMask];
710 
711  // Move the window to the specified screen.
712  // (Necessary even for Mac-fullscreen mode, since it fullscreens on whichever screen the window is currently on.)
713  if (screen && ![[self screen] isEqualTo:screen])
714  {
715  NSRect windowFrame = self.frame;
716  NSRect screenFrame = screen.frame;
717  self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
718  screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
719  }
720 
721 
722  // Use Mac-fullscreen mode by default, since it's compatible with Mission Control,
723  // and since (unlike Kiosk Mode) the menu bar stays hidden on the fullscreen display
724  // when you focus a window on a different display (such as when livecoding during a performance).
725  bool useMacFullScreenMode = true;
726 
727  // If `System Preferences > Mission Control > Displays have separate Spaces` is unchecked,
728  // only a single window could go Mac-fullscreen at once, so don't use Mac-fullscreen mode in that case.
729  if (!NSScreen.screensHaveSeparateSpaces)
730  useMacFullScreenMode = false;
731 
732 
733  if (useMacFullScreenMode)
734  {
735  _isInMacFullScreenMode = YES;
736 
737  // If a size-locked window enters Mac-fullscreen mode,
738  // its height increases upon exiting Mac-fullscreen mode.
739  // Mark it resizeable while fullscreen, to keep its size from changing (!).
740  self.styleMask |= NSResizableWindowMask;
741 
742  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
743  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
744  dispatch_async(dispatch_get_main_queue(), ^{
745  _programmaticallyTransitioningFullScreen = true;
746  [self toggleFullScreen:nil];
747  [self updateFullScreenMenu];
748  });
749  });
750  }
751  else
752  {
753  [self setFullScreenPresentation:YES];
754 
755  NSSize car = self.contentAspectRatio;
756  self.styleMask = 0;
757  if (!NSEqualSizes(car, NSMakeSize(0,0)))
758  self.contentAspectRatio = car;
759 
760  // Make the window take up the whole screen.
761  [self setFrame:self.screen.frame display:YES];
762 
763  [self finishFullScreenTransition];
764  [self updateFullScreenMenu];
765  }
766  }
767  else if (!wantsFullScreen && [self isFullScreen])
768  {
769  // Switch out of fullscreen mode.
770 
771  if (_isInMacFullScreenMode)
772  {
773  _isInMacFullScreenMode = NO;
774 
775  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
776  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
777  dispatch_async(dispatch_get_main_queue(), ^{
778  _programmaticallyTransitioningFullScreen = true;
779  [self toggleFullScreen:nil];
780  self.styleMask = _styleMaskWhenWindowed;
781  [self updateFullScreenMenu];
782  });
783  });
784  }
785  else
786  {
787  [self setFullScreenPresentation:NO];
788 
789  NSSize car = self.contentAspectRatio;
790  self.styleMask = _styleMaskWhenWindowed;
791  self.title = _titleBackup;
792  if (!NSEqualSizes(car, NSMakeSize(0,0)))
793  self.contentAspectRatio = car;
794 
795  [self setFrame:[self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
796 
797  [self finishFullScreenTransition];
798  [self updateFullScreenMenu];
799  }
800  }
801 }
802 
809 {
810  _programmaticallyResizingWindow = false;
811 
812  [self.delegate windowDidResize:nil];
813 
814  [self makeFirstResponder:self];
815 
816  if (self.shouldGoFullscreen)
817  dispatch_async(dispatch_get_main_queue(), ^{
818  NSScreen *s = [self.shouldGoFullscreen retain];
819  self.shouldGoFullscreen = nil;
820  [self setFullScreen:true onScreen:s];
821  [s release];
822  });
823 }
824 
830 - (void)toggleFullScreen
831 {
832  [self setFullScreen:![self isFullScreen] onScreen:nil];
833 }
834 
839 - (void)scheduleResize:(NSSize)newSize
840 {
841  VDebugLog("Requested to resize to %gx%g points.",newSize.width,newSize.height);
842  if (_leftMouseDown)
843  {
844  VDebugLog(" The window is being dragged; deferring the resize.");
845  _pendingResize = newSize;
846  }
847  else
848  {
849  VDebugLog(" The window is not being dragged; resizing now.");
850  self.contentSize = newSize;
851  }
852 }
853 
859 - (void)toggleRecording
860 {
861  if (!self.recorder)
862  {
863  // Start recording to a temporary file (rather than first asking for the filename, to make it quicker to start recording).
864  self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
865  _recorder = [[VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
866 
867  [self updateUI];
868  }
869  else
870  [self stopRecording];
871 }
872 
876 - (void)promptToSaveMovie
877 {
878  NSSavePanel *sp = [NSSavePanel savePanel];
879  [sp setTitle:@"Save Movie"];
880  [sp setNameFieldLabel:@"Save Movie To:"];
881  [sp setPrompt:@"Save"];
882  [sp setAllowedFileTypes:@[@"mov"]];
883 
884  char *title = VuoApp_getName();
885  sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
886  free(title);
887 
888  if ([sp runModal] == NSFileHandlingPanelCancelButton)
889  goto done;
890 
891  NSError *error;
892  if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
893  {
894  if ([error code] == NSFileWriteFileExistsError)
895  {
896  // File exists. Since, in the NSSavePanel, the user said to Replace, try replacing it.
897  if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
898  withItemAtURL:_temporaryMovieURL
899  backupItemName:nil
900  options:0
901  resultingItemURL:nil
902  error:&error])
903  {
904  // Replacement failed; show error…
905  NSAlert *alert = [NSAlert alertWithError:error];
906  [alert runModal];
907 
908  // …and give the user another chance.
909  [self promptToSaveMovie];
910  }
911  goto done;
912  }
913 
914  NSAlert *alert = [NSAlert alertWithError:error];
915  [alert runModal];
916  }
917 
918 done:
919  self.temporaryMovieURL = nil;
920 }
921 
929 - (void)stopRecording
930 {
931  if (!_recorder)
932  return;
933 
934  [_recorder finish];
935  [_recorder release];
936  _recorder = nil;
937 
938  [self updateUI];
939 
940  [self setFullScreen:NO onScreen:nil];
941 
942  [self promptToSaveMovie];
943 }
944 
950 - (void)cancelOperation:(id)sender
951 {
952  [self setFullScreen:NO onScreen:nil];
953 }
954 
958 - (void)close
959 {
960  NSView *v = self.contentView;
961  VuoGraphicsLayer *gv = (VuoGraphicsLayer *)v.layer;
962  [gv close];
963 
964  [super close];
965 }
966 
970 - (void)dealloc
971 {
972  [NSEvent removeMonitor:_mouseMonitor];
973 
974  // https://b33p.net/kosada/node/12123
975  // Cocoa leaks the contentView unless we force it to resign firstResponder status.
976  [self makeFirstResponder:nil];
977 
978  [super dealloc];
979 }
980 
981 @end