Vuo  2.0.3
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 @end
65 
66 @implementation VuoGraphicsWindow
67 
71 - (instancetype)initWithInitCallback:(VuoGraphicsWindowInitCallback)initCallback
72  updateBackingCallback:(VuoGraphicsWindowUpdateBackingCallback)updateBackingCallback
73  resizeCallback:(VuoGraphicsWindowResizeCallback)resizeCallback
74  drawCallback:(VuoGraphicsWindowDrawCallback)drawCallback
75  userData:(void *)userData
76 {
77  NSRect mainScreenFrame = [[NSScreen mainScreen] frame];
78  _contentRectWhenWindowed = NSMakeRect(mainScreenFrame.origin.x, mainScreenFrame.origin.y, VuoGraphicsWindowDefaultWidth, 768);
79  _styleMaskWhenWindowed = NSTitledWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask;
80  if (self = [super initWithContentRect:_contentRectWhenWindowed
81  styleMask:_styleMaskWhenWindowed
82  backing:NSBackingStoreBuffered
83  defer:NO])
84  {
85  self.colorSpace = NSColorSpace.sRGBColorSpace;
86 
87  self.privateDelegate = [[[VuoGraphicsWindowDelegate alloc] initWithWindow:self] autorelease];
88  self.delegate = self.privateDelegate;
89  self.releasedWhenClosed = NO;
90 
91  _cursor = VuoCursor_Pointer;
92 
93  self.contentMinSize = NSMakeSize(VuoGraphicsWindowMinSize, VuoGraphicsWindowMinSize);
94 
95  self.acceptsMouseMovedEvents = YES;
96 
97  self.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
98 
99  [self initDrag];
100  [self registerForDraggedTypes:@[NSFilenamesPboardType]];
101 
102  _userResizedWindow = NO;
103  _programmaticallyResizingWindow = NO;
104  _constrainToScreen = YES;
105 
106  char *title = VuoApp_getName();
107  self.title = [NSString stringWithUTF8String:title];
108  free(title);
109 
110  _backingScaleFactorCached = self.backingScaleFactor;
111 
112  VuoGraphicsLayer *l = [[VuoGraphicsLayer alloc] initWithWindow:self
113  initCallback:initCallback
114  updateBackingCallback:updateBackingCallback
115  backingScaleFactor:_backingScaleFactorCached
116  resizeCallback:resizeCallback
117  drawCallback:drawCallback
118  userData:userData];
119  l.contentsScale = _backingScaleFactorCached;
120 
121  VuoGraphicsView *v = [[VuoGraphicsView alloc] init];
122  v.layer = l;
123  [l release];
124 
125  self.contentView = v;
126  [v release];
127 
128  _contentRectCached = l.frame;
129  }
130 
131  return self;
132 }
133 
140 - (void)draw
141 {
142  __block VuoGraphicsLayer *l;
144  NSView *v = self.contentView;
145  l = (VuoGraphicsLayer *)v.layer;
146  });
147  [l draw];
148 }
149 
153 - (BOOL)canBecomeMainWindow
154 {
155  return YES;
156 }
157 
161 - (BOOL)canBecomeKeyWindow
162 {
163  return YES;
164 }
165 
166 - (void)updateFullScreenMenu
167 {
168  NSMenuItem *fullScreenMenuItem = [[[[[NSApplication sharedApplication] mainMenu] itemWithTag:VuoViewMenuItemTag] submenu] itemWithTag:VuoFullScreenMenuItemTag];
169  fullScreenMenuItem.title = self.isFullScreen ? @"Exit Full Screen" : @"Enter Full Screen";
170 }
171 
177 - (void)becomeMainWindow
178 {
179  [super becomeMainWindow];
180 
181  if (!_isInMacFullScreenMode)
182  [self setFullScreenPresentation:self.isFullScreen];
183 
184  NSMenu *fileMenu = [[[NSMenu alloc] initWithTitle:@"File"] autorelease];
185  _recordMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleRecording) keyEquivalent:@"e"];
186  [_recordMenuItem setKeyEquivalentModifierMask:NSCommandKeyMask|NSAlternateKeyMask];
187  [fileMenu addItem:_recordMenuItem];
188  NSMenuItem *fileMenuItem = [[NSMenuItem new] autorelease];
189  [fileMenuItem setSubmenu:fileMenu];
190 
191  NSMenu *viewMenu = [[[NSMenu alloc] initWithTitle:@"View"] autorelease];
192  NSMenuItem *fullScreenMenuItem = [[[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleFullScreen) keyEquivalent:@"f"] autorelease];
193  fullScreenMenuItem.tag = VuoFullScreenMenuItemTag;
194  [viewMenu addItem:fullScreenMenuItem];
195  NSMenuItem *viewMenuItem = [[NSMenuItem new] autorelease];
196  viewMenuItem.tag = VuoViewMenuItemTag;
197  [viewMenuItem setSubmenu:viewMenu];
198 
199  NSMenu *windowMenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
200  NSMenuItem *minimizeMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease];
201  NSMenuItem *zoomMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""] autorelease];
202  NSMenuItem *cycleMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Cycle Through Windows" action:@selector(_cycleWindows:) keyEquivalent:@"`"] autorelease];
203  [windowMenu addItem:minimizeMenuItem];
204  [windowMenu addItem:zoomMenuItem];
205  [windowMenu addItem:cycleMenuItem];
206  NSMenuItem *windowMenuItem = [[NSMenuItem new] autorelease];
207  [windowMenuItem setSubmenu:windowMenu];
208 
209  NSMutableArray *windowMenuItems = [NSMutableArray arrayWithCapacity:3];
210  [windowMenuItems addObject:fileMenuItem];
211  [windowMenuItems addObject:viewMenuItem];
212  [windowMenuItems addObject:windowMenuItem];
213  self.oldMenu = (NSMenu *)VuoApp_setMenuItems(windowMenuItems);
214 
215  [self updateFullScreenMenu];
216 
217  [self updateUI];
218 }
219 
223 - (void)resignMainWindow
224 {
225  [super resignMainWindow];
226 
227  // No need to change presentation when resigning, since:
228  // - other VuoGraphicsWindows change presentation on becomeMainWindow
229  // - Mac OS X changes presentation when another app becomes active
230  //
231  // Also, if two VuoGraphicsWindows are fullscreen on separate displays,
232  // switching out of fullscreen presentation will cause the menubar and dock to flicker
233  // when clicking from one display to another.
234 // if (!_isInMacFullScreenMode)
235 // [self setFullScreenPresentation:NO];
236 
237  VuoApp_setMenu(_oldMenu);
238  self.oldMenu = nil;
239 }
240 
244 - (void)keyDown:(NSEvent *)event
245 {
246  if ([event keyCode] == kVK_Escape && [self isFullScreen])
247  [super keyDown:event];
248 }
249 
253 - (void)updateUI
254 {
255  if (_recorder)
256  _recordMenuItem.title = @"Stop Recording…";
257  else
258  _recordMenuItem.title = @"Start Recording";
259 }
260 
266 - (void)enableUpdatedWindowTrigger:(VuoGraphicsWindowUpdatedWindowCallback)updatedWindow
267 {
268  _updatedWindow = updatedWindow;
269 
272  _updatedWindow(rl);
273 
274  __block VuoGraphicsLayer *l;
276  NSView *v = self.contentView;
277  l = (VuoGraphicsLayer *)v.layer;
278  });
279  [l enableTriggers];
280 }
281 
288 - (void)enableShowedWindowTrigger:(VuoGraphicsWindowShowedWindowCallback)showedWindow requestedFrameTrigger:(VuoGraphicsWindowRequestedFrameCallback)requestedFrame
289 {
290  _showedWindow = showedWindow;
291  _showedWindow(VuoWindowReference_make(self));
292 
293  _requestedFrame = requestedFrame;
294 
295  __block VuoGraphicsLayer *l;
297  NSView *v = self.contentView;
298  l = (VuoGraphicsLayer *)v.layer;
299  });
300  [l enableTriggers];
301 }
302 
309 {
310  __block VuoGraphicsLayer *l;
312  NSView *v = self.contentView;
313  l = (VuoGraphicsLayer *)v.layer;
314  });
315  [l disableTriggers];
316 
317  _updatedWindow = NULL;
318  _showedWindow = NULL;
319  _requestedFrame = NULL;
320 }
321 
328 {
329  __block NSUInteger styleMask;
331  styleMask = self.styleMask;
332  });
333  return _isInMacFullScreenMode || styleMask == 0;
334 }
335 
336 
340 - (void)setTitle:(NSString *)title
341 {
342  self.titleBackup = title;
343  super.title = title;
344 }
345 
351 - (void)setProperties:(VuoList_VuoWindowProperty)properties
352 {
353  unsigned int propertyCount = VuoListGetCount_VuoWindowProperty(properties);
354  for (unsigned int i = 1; i <= propertyCount; ++i)
355  {
356  VuoWindowProperty property = VuoListGetValue_VuoWindowProperty(properties, i);
357  VDebugLog("%s", VuoWindowProperty_getSummary(property));
358 
359  if (property.type == VuoWindowProperty_Title)
360  {
361  self.title = property.title ? [NSString stringWithUTF8String:property.title] : @"";
362  if (_updatedWindow)
363  {
366  _updatedWindow(rl);
367  }
368  }
369  else if (property.type == VuoWindowProperty_FullScreen)
370  {
371  NSScreen *requestedScreen = VuoScreen_getNSScreen(property.screen);
372 
373  bool isFullScreen = self.isFullScreen;
374  bool wantsFullScreen = property.fullScreen;
375 
376  // Only go fullscreen if the specific requested screen exists.
377  // https://b33p.net/kosada/node/14658
378  if (wantsFullScreen && !requestedScreen)
379  continue;
380 
381  NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
382  NSInteger currentDeviceId = [[[self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
383  bool changingScreen = requestedDeviceId != currentDeviceId;
384 
385  if (isFullScreen && wantsFullScreen && changingScreen)
386  {
387  // Temporarily switch back to windowed mode so that we can switch to fullscreen on the new screen
388  // (since macOS doesn't let us directly move from one fullscreen to another).
389  [self setFullScreen:NO onScreen:nil];
390 
391  // If `System Preferences > Mission Control > Displays have separate Spaces` is enabled…
392  if (NSScreen.screensHaveSeparateSpaces)
393  // Give macOS a chance to complete its exit-fullscreen transition.
394  self.shouldGoFullscreen = requestedScreen;
395  else
396  // If not, we can immediately go fullscreen on the new screen.
397  [self setFullScreen:YES onScreen:requestedScreen];
398  }
399  else if (isFullScreen != wantsFullScreen)
400  [self setFullScreen:property.fullScreen onScreen:requestedScreen];
401  else if (requestedScreen && !isFullScreen)
402  // Move the non-fullscreen window to the center of the specified screen.
403  [self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
404  NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
405  }
406  else if (property.type == VuoWindowProperty_Position)
407  {
408  NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
409  if (property.unit == VuoCoordinateUnit_Pixels)
410  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
411 
412  NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
413  if ([self isFullScreen])
414  _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
415  else
416  {
417  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
418  self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
419  }
420  }
421  else if (property.type == VuoWindowProperty_Size)
422  {
423  NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
424  if (property.unit == VuoCoordinateUnit_Pixels)
425  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
426  _maintainsPixelSizeWhenBackingChanges = (property.unit == VuoCoordinateUnit_Pixels);
427 
428  if ([self isFullScreen])
429  _contentRectWhenWindowed.size = propertyInPoints.size;
430  else
431  {
432  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
433 
434  // Adjust the y position by the change in height, so that the window appears to be anchored in its top-left corner
435  // (instead of its bottom-left corner as the system does by default).
436  contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
437 
438  contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
439  @try
440  {
441  _constrainToScreen = NO;
442  [self setFrame:[self frameRectForContentRect:contentRect] display:YES animate:NO];
443  _constrainToScreen = YES;
444  }
445  @catch (NSException *e)
446  {
447  VuoText description = VuoText_makeFromCFString(e.description);
448  VuoLocal(description);
449  VUserLog("Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
450  }
451  }
452  }
453  else if (property.type == VuoWindowProperty_AspectRatio)
454  {
455  if (property.aspectRatio < 1./10000
456  || property.aspectRatio > 10000)
457  {
458  VUserLog("Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
459  continue;
460  }
461 
462  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
463  [self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
464  }
465  else if (property.type == VuoWindowProperty_AspectRatioReset)
466  [self unlockAspectRatio];
467  else if (property.type == VuoWindowProperty_Resizable)
468  {
469  [[self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
470  if ([self isFullScreen])
471  _styleMaskWhenWindowed = property.resizable ? (_styleMaskWhenWindowed | NSResizableWindowMask) : (_styleMaskWhenWindowed & ~NSResizableWindowMask);
472  else
473  self.styleMask = property.resizable ? ([self styleMask] | NSResizableWindowMask) : ([self styleMask] & ~NSResizableWindowMask);
474  }
475  else if (property.type == VuoWindowProperty_Cursor)
476  {
477  _cursor = property.cursor;
478  [self invalidateCursorRectsForView:self.contentView];
479  }
480  }
481 }
482 
489 - (NSRect)liveFrame
490 {
491  Rect qdRect;
492  extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
493 
494  GetWindowBounds([self windowRef], kWindowStructureRgn, &qdRect);
495 
496  return NSMakeRect(qdRect.left,
497  (float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
498  qdRect.right - qdRect.left,
499  qdRect.bottom - qdRect.top);
500 }
501 
512 - (void)setAspectRatioToWidth:(unsigned int)pixelsWide height:(unsigned int)pixelsHigh
513 {
514  pixelsWide = MAX(VuoGraphicsWindowMinSize, pixelsWide);
515  pixelsHigh = MAX(VuoGraphicsWindowMinSize, pixelsHigh);
516 
517  NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
518  if (NSEqualSizes([self contentAspectRatio], newAspect))
519  return;
520 
521  // Sets the constraint when the user resizes the window (but doesn't affect the window's current size).
522  self.contentAspectRatio = newAspect;
523 
524  if ([self isFullScreen])
525  {
526  if (_updatedWindow)
527  {
530  _updatedWindow(rl);
531  }
532  if (_showedWindow)
533  _showedWindow(VuoWindowReference_make(self));
534  return;
535  }
536 
537  CGFloat desiredWidth = pixelsWide;
538  CGFloat desiredHeight = pixelsHigh;
539  CGRect windowFrame = [self liveFrame];
540  CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
541 
542  // Adjust the width and height if the user manually resized the window.
543  if (_userResizedWindow)
544  {
545  // Preserve the width, scale the height.
546  desiredWidth = CGRectGetWidth(windowFrame);
547  desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
548  }
549 
550  // Adjust the width and height if they don't fit the screen.
551  NSRect screenFrame = [[self screen] visibleFrame];
552  NSRect maxContentRect = [self contentRectForFrameRect:screenFrame];
553  if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
554  {
555  CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
556  if (aspectRatio >= maxContentAspectRatio)
557  {
558  // Too wide, so scale it to the maximum horizontal screen space.
559  desiredWidth = maxContentRect.size.width;
560  desiredHeight = maxContentRect.size.width / aspectRatio;
561  }
562  else
563  {
564  // Too tall, so scale it to the maximum vertical screen space.
565  desiredWidth = maxContentRect.size.height * aspectRatio;
566  desiredHeight = maxContentRect.size.height;
567  }
568  }
569 
570  NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
571  NSSize newWindowSize = [self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
572 
573  // Preserve the window's top left corner position. (If you just resize, it preserves the bottom left corner position.)
574  CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
575  NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
576 
577  _programmaticallyResizingWindow = YES;
578  [self setFrame:newWindowFrame display:YES];
579  _programmaticallyResizingWindow = NO;
580 
581  if (_updatedWindow)
582  {
585  _updatedWindow(rl);
586  }
587  if (_showedWindow)
588  _showedWindow(VuoWindowReference_make(self));
589 }
590 
597 {
598  self.resizeIncrements = NSMakeSize(1,1);
599 }
600 
605 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
606 {
607  if (_constrainToScreen)
608  return [super constrainFrameRect:frameRect toScreen:screen];
609  else
610  return frameRect;
611 }
612 
616 - (void)setFullScreenPresentation:(bool)enabled
617 {
618  if (enabled)
619  {
620  // Don't raise the window level, since the window still covers the screen even when it's not focused.
621 // self.level = NSScreenSaverWindowLevel;
622 
623  // Instead, just hide the dock and menu bar.
624  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
625  }
626  else
627  {
628 // self.level = NSNormalWindowLevel;
629  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
630  }
631 }
632 
669 - (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
670 {
671  _programmaticallyResizingWindow = true;
672 
673  if (wantsFullScreen && ![self isFullScreen])
674  {
675  // Switch into fullscreen mode.
676 
677 
678  // Save the position, size, and style, to be restored when switching back to windowed mode.
679  _contentRectWhenWindowed = [self contentRectForFrameRect:self.frame];
680  _styleMaskWhenWindowed = [self styleMask];
681 
682  // Move the window to the specified screen.
683  // (Necessary even for Mac-fullscreen mode, since it fullscreens on whichever screen the window is currently on.)
684  if (screen && ![[self screen] isEqualTo:screen])
685  {
686  NSRect windowFrame = self.frame;
687  NSRect screenFrame = screen.frame;
688  self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
689  screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
690  }
691 
692 
693  // Use Mac-fullscreen mode by default, since it's compatible with Mission Control,
694  // and since (unlike Kiosk Mode) the menu bar stays hidden on the fullscreen display
695  // when you focus a window on a different display (such as when livecoding during a performance).
696  bool useMacFullScreenMode = true;
697 
698  // If `System Preferences > Mission Control > Displays have separate Spaces` is unchecked,
699  // only a single window could go Mac-fullscreen at once, so don't use Mac-fullscreen mode in that case.
700  if (!NSScreen.screensHaveSeparateSpaces)
701  useMacFullScreenMode = false;
702 
703 
704  if (useMacFullScreenMode)
705  {
706  _isInMacFullScreenMode = YES;
707 
708  // If a size-locked window enters Mac-fullscreen mode,
709  // its height increases upon exiting Mac-fullscreen mode.
710  // Mark it resizeable while fullscreen, to keep its size from changing (!).
711  self.styleMask |= NSResizableWindowMask;
712 
713  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
714  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
715  dispatch_async(dispatch_get_main_queue(), ^{
716  _programmaticallyTransitioningFullScreen = true;
717  [self toggleFullScreen:nil];
718  [self updateFullScreenMenu];
719  });
720  });
721  }
722  else
723  {
724  [self setFullScreenPresentation:YES];
725 
726  NSSize car = self.contentAspectRatio;
727  self.styleMask = 0;
728  if (!NSEqualSizes(car, NSMakeSize(0,0)))
729  self.contentAspectRatio = car;
730 
731  // Make the window take up the whole screen.
732  [self setFrame:self.screen.frame display:YES];
733 
734  [self finishFullScreenTransition];
735  [self updateFullScreenMenu];
736  }
737  }
738  else if (!wantsFullScreen && [self isFullScreen])
739  {
740  // Switch out of fullscreen mode.
741 
742  if (_isInMacFullScreenMode)
743  {
744  _isInMacFullScreenMode = NO;
745 
746  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
747  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
748  dispatch_async(dispatch_get_main_queue(), ^{
749  _programmaticallyTransitioningFullScreen = true;
750  [self toggleFullScreen:nil];
751  self.styleMask = _styleMaskWhenWindowed;
752  [self updateFullScreenMenu];
753  });
754  });
755  }
756  else
757  {
758  [self setFullScreenPresentation:NO];
759 
760  NSSize car = self.contentAspectRatio;
761  self.styleMask = _styleMaskWhenWindowed;
762  self.title = _titleBackup;
763  if (!NSEqualSizes(car, NSMakeSize(0,0)))
764  self.contentAspectRatio = car;
765 
766  [self setFrame:[self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
767 
768  [self finishFullScreenTransition];
769  [self updateFullScreenMenu];
770  }
771  }
772 }
773 
780 {
781  _programmaticallyResizingWindow = false;
782 
783  [self.delegate windowDidResize:nil];
784 
785  [self makeFirstResponder:self];
786 
787  if (self.shouldGoFullscreen)
788  dispatch_async(dispatch_get_main_queue(), ^{
789  NSScreen *s = [self.shouldGoFullscreen retain];
790  self.shouldGoFullscreen = nil;
791  [self setFullScreen:true onScreen:s];
792  [s release];
793  });
794 }
795 
801 - (void)toggleFullScreen
802 {
803  [self setFullScreen:![self isFullScreen] onScreen:nil];
804 }
805 
811 - (void)toggleRecording
812 {
813  if (!self.recorder)
814  {
815  // Start recording to a temporary file (rather than first asking for the filename, to make it quicker to start recording).
816  self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
817  _recorder = [[VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
818 
819  [self updateUI];
820  }
821  else
822  [self stopRecording];
823 }
824 
828 - (void)promptToSaveMovie
829 {
830  NSSavePanel *sp = [NSSavePanel savePanel];
831  [sp setTitle:@"Save Movie"];
832  [sp setNameFieldLabel:@"Save Movie To:"];
833  [sp setPrompt:@"Save"];
834  [sp setAllowedFileTypes:@[@"mov"]];
835 
836  char *title = VuoApp_getName();
837  sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
838  free(title);
839 
840  if ([sp runModal] == NSFileHandlingPanelCancelButton)
841  goto done;
842 
843  NSError *error;
844  if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
845  {
846  if ([error code] == NSFileWriteFileExistsError)
847  {
848  // File exists. Since, in the NSSavePanel, the user said to Replace, try replacing it.
849  if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
850  withItemAtURL:_temporaryMovieURL
851  backupItemName:nil
852  options:0
853  resultingItemURL:nil
854  error:&error])
855  {
856  // Replacement failed; show error…
857  NSAlert *alert = [NSAlert alertWithError:error];
858  [alert runModal];
859 
860  // …and give the user another chance.
861  [self promptToSaveMovie];
862  }
863  goto done;
864  }
865 
866  NSAlert *alert = [NSAlert alertWithError:error];
867  [alert runModal];
868  }
869 
870 done:
871  self.temporaryMovieURL = nil;
872 }
873 
881 - (void)stopRecording
882 {
883  if (!_recorder)
884  return;
885 
886  [_recorder finish];
887  [_recorder release];
888  _recorder = nil;
889 
890  [self updateUI];
891 
892  [self setFullScreen:NO onScreen:nil];
893 
894  [self promptToSaveMovie];
895 }
896 
902 - (void)cancelOperation:(id)sender
903 {
904  [self setFullScreen:NO onScreen:nil];
905 }
906 
910 - (void)close
911 {
912  NSView *v = self.contentView;
913  VuoGraphicsLayer *gv = (VuoGraphicsLayer *)v.layer;
914  [gv close];
915 
916  [super close];
917 }
918 
922 - (void)dealloc
923 {
924  // https://b33p.net/kosada/node/12123
925  // Cocoa leaks the contentView unless we force it to resign firstResponder status.
926  [self makeFirstResponder:nil];
927 
928  [super dealloc];
929 }
930 
931 @end