Vuo  2.0.0
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  self.title = property.title ? [NSString stringWithUTF8String:property.title] : @"";
361  else if (property.type == VuoWindowProperty_FullScreen)
362  {
363  NSScreen *requestedScreen = VuoScreen_getNSScreen(property.screen);
364 
365  bool isFullScreen = self.isFullScreen;
366  bool wantsFullScreen = property.fullScreen;
367 
368  // Only go fullscreen if the specific requested screen exists.
369  // https://b33p.net/kosada/node/14658
370  if (wantsFullScreen && !requestedScreen)
371  continue;
372 
373  NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
374  NSInteger currentDeviceId = [[[self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
375  bool changingScreen = requestedDeviceId != currentDeviceId;
376 
377  if (isFullScreen && wantsFullScreen && changingScreen)
378  {
379  // Temporarily switch back to windowed mode so that we can switch to fullscreen on the new screen
380  // (since macOS doesn't let us directly move from one fullscreen to another).
381  [self setFullScreen:NO onScreen:nil];
382 
383  // If `System Preferences > Mission Control > Displays have separate Spaces` is enabled…
384  if (NSScreen.screensHaveSeparateSpaces)
385  // Give macOS a chance to complete its exit-fullscreen transition.
386  self.shouldGoFullscreen = requestedScreen;
387  else
388  // If not, we can immediately go fullscreen on the new screen.
389  [self setFullScreen:YES onScreen:requestedScreen];
390  }
391  else if (isFullScreen != wantsFullScreen)
392  [self setFullScreen:property.fullScreen onScreen:requestedScreen];
393  else if (requestedScreen && !isFullScreen)
394  // Move the non-fullscreen window to the center of the specified screen.
395  [self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
396  NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
397  }
398  else if (property.type == VuoWindowProperty_Position)
399  {
400  NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
401  if (property.unit == VuoCoordinateUnit_Pixels)
402  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
403 
404  NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
405  if ([self isFullScreen])
406  _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
407  else
408  {
409  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
410  self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
411  }
412  }
413  else if (property.type == VuoWindowProperty_Size)
414  {
415  NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
416  if (property.unit == VuoCoordinateUnit_Pixels)
417  propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
418  _maintainsPixelSizeWhenBackingChanges = (property.unit == VuoCoordinateUnit_Pixels);
419 
420  if ([self isFullScreen])
421  _contentRectWhenWindowed.size = propertyInPoints.size;
422  else
423  {
424  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
425 
426  // Adjust the y position by the change in height, so that the window appears to be anchored in its top-left corner
427  // (instead of its bottom-left corner as the system does by default).
428  contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
429 
430  contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
431  @try
432  {
433  _constrainToScreen = NO;
434  [self setFrame:[self frameRectForContentRect:contentRect] display:YES animate:NO];
435  _constrainToScreen = YES;
436  }
437  @catch (NSException *e)
438  {
439  char *description = VuoLog_copyCFDescription(e);
440  VUserLog("Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
441  free(description);
442  }
443  }
444  }
445  else if (property.type == VuoWindowProperty_AspectRatio)
446  {
447  if (property.aspectRatio < 1./10000
448  || property.aspectRatio > 10000)
449  {
450  VUserLog("Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
451  continue;
452  }
453 
454  NSRect contentRect = [self contentRectForFrameRect:[self frame]];
455  [self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
456  }
457  else if (property.type == VuoWindowProperty_AspectRatioReset)
458  [self unlockAspectRatio];
459  else if (property.type == VuoWindowProperty_Resizable)
460  {
461  [[self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
462  if ([self isFullScreen])
463  _styleMaskWhenWindowed = property.resizable ? (_styleMaskWhenWindowed | NSResizableWindowMask) : (_styleMaskWhenWindowed & ~NSResizableWindowMask);
464  else
465  self.styleMask = property.resizable ? ([self styleMask] | NSResizableWindowMask) : ([self styleMask] & ~NSResizableWindowMask);
466  }
467  else if (property.type == VuoWindowProperty_Cursor)
468  {
469  _cursor = property.cursor;
470  [self invalidateCursorRectsForView:self.contentView];
471  }
472  }
473 }
474 
481 - (NSRect)liveFrame
482 {
483  Rect qdRect;
484  extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
485 
486  GetWindowBounds([self windowRef], kWindowStructureRgn, &qdRect);
487 
488  return NSMakeRect(qdRect.left,
489  (float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
490  qdRect.right - qdRect.left,
491  qdRect.bottom - qdRect.top);
492 }
493 
504 - (void)setAspectRatioToWidth:(unsigned int)pixelsWide height:(unsigned int)pixelsHigh
505 {
506  pixelsWide = MAX(VuoGraphicsWindowMinSize, pixelsWide);
507  pixelsHigh = MAX(VuoGraphicsWindowMinSize, pixelsHigh);
508 
509  NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
510  if (NSEqualSizes([self contentAspectRatio], newAspect))
511  return;
512 
513  // Sets the constraint when the user resizes the window (but doesn't affect the window's current size).
514  self.contentAspectRatio = newAspect;
515 
516  if ([self isFullScreen])
517  {
518  if (_updatedWindow)
519  {
522  _updatedWindow(rl);
523  }
524  if (_showedWindow)
525  _showedWindow(VuoWindowReference_make(self));
526  return;
527  }
528 
529  CGFloat desiredWidth = pixelsWide;
530  CGFloat desiredHeight = pixelsHigh;
531  CGRect windowFrame = [self liveFrame];
532  CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
533 
534  // Adjust the width and height if the user manually resized the window.
535  if (_userResizedWindow)
536  {
537  // Preserve the width, scale the height.
538  desiredWidth = CGRectGetWidth(windowFrame);
539  desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
540  }
541 
542  // Adjust the width and height if they don't fit the screen.
543  NSRect screenFrame = [[self screen] visibleFrame];
544  NSRect maxContentRect = [self contentRectForFrameRect:screenFrame];
545  if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
546  {
547  CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
548  if (aspectRatio >= maxContentAspectRatio)
549  {
550  // Too wide, so scale it to the maximum horizontal screen space.
551  desiredWidth = maxContentRect.size.width;
552  desiredHeight = maxContentRect.size.width / aspectRatio;
553  }
554  else
555  {
556  // Too tall, so scale it to the maximum vertical screen space.
557  desiredWidth = maxContentRect.size.height * aspectRatio;
558  desiredHeight = maxContentRect.size.height;
559  }
560  }
561 
562  NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
563  NSSize newWindowSize = [self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
564 
565  // Preserve the window's top left corner position. (If you just resize, it preserves the bottom left corner position.)
566  CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
567  NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
568 
569  _programmaticallyResizingWindow = YES;
570  [self setFrame:newWindowFrame display:YES];
571  _programmaticallyResizingWindow = NO;
572 
573  if (_updatedWindow)
574  {
577  _updatedWindow(rl);
578  }
579  if (_showedWindow)
580  _showedWindow(VuoWindowReference_make(self));
581 }
582 
589 {
590  self.resizeIncrements = NSMakeSize(1,1);
591 }
592 
597 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
598 {
599  if (_constrainToScreen)
600  return [super constrainFrameRect:frameRect toScreen:screen];
601  else
602  return frameRect;
603 }
604 
608 - (void)setFullScreenPresentation:(bool)enabled
609 {
610  if (enabled)
611  {
612  // Don't raise the window level, since the window still covers the screen even when it's not focused.
613 // self.level = NSScreenSaverWindowLevel;
614 
615  // Instead, just hide the dock and menu bar.
616  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
617  }
618  else
619  {
620 // self.level = NSNormalWindowLevel;
621  ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
622  }
623 }
624 
661 - (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
662 {
663  _programmaticallyResizingWindow = true;
664 
665  if (wantsFullScreen && ![self isFullScreen])
666  {
667  // Switch into fullscreen mode.
668 
669 
670  // Save the position, size, and style, to be restored when switching back to windowed mode.
671  _contentRectWhenWindowed = [self contentRectForFrameRect:self.frame];
672  _styleMaskWhenWindowed = [self styleMask];
673 
674  // Move the window to the specified screen.
675  // (Necessary even for Mac-fullscreen mode, since it fullscreens on whichever screen the window is currently on.)
676  if (screen && ![[self screen] isEqualTo:screen])
677  {
678  NSRect windowFrame = self.frame;
679  NSRect screenFrame = screen.frame;
680  self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
681  screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
682  }
683 
684 
685  // Use Mac-fullscreen mode by default, since it's compatible with Mission Control,
686  // and since (unlike Kiosk Mode) the menu bar stays hidden on the fullscreen display
687  // when you focus a window on a different display (such as when livecoding during a performance).
688  bool useMacFullScreenMode = true;
689 
690  // If `System Preferences > Mission Control > Displays have separate Spaces` is unchecked,
691  // only a single window could go Mac-fullscreen at once, so don't use Mac-fullscreen mode in that case.
692  if (!NSScreen.screensHaveSeparateSpaces)
693  useMacFullScreenMode = false;
694 
695 
696  if (useMacFullScreenMode)
697  {
698  _isInMacFullScreenMode = YES;
699 
700  // If a size-locked window enters Mac-fullscreen mode,
701  // its height increases upon exiting Mac-fullscreen mode.
702  // Mark it resizeable while fullscreen, to keep its size from changing (!).
703  self.styleMask |= NSResizableWindowMask;
704 
705  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
706  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
707  dispatch_async(dispatch_get_main_queue(), ^{
708  _programmaticallyTransitioningFullScreen = true;
709  [self toggleFullScreen:nil];
710  [self updateFullScreenMenu];
711  });
712  });
713  }
714  else
715  {
716  [self setFullScreenPresentation:YES];
717 
718  NSSize car = self.contentAspectRatio;
719  self.styleMask = 0;
720  if (!NSEqualSizes(car, NSMakeSize(0,0)))
721  self.contentAspectRatio = car;
722 
723  // Make the window take up the whole screen.
724  [self setFrame:self.screen.frame display:YES];
725 
726  [self finishFullScreenTransition];
727  [self updateFullScreenMenu];
728  }
729  }
730  else if (!wantsFullScreen && [self isFullScreen])
731  {
732  // Switch out of fullscreen mode.
733 
734  if (_isInMacFullScreenMode)
735  {
736  _isInMacFullScreenMode = NO;
737 
738  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
739  dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
740  dispatch_async(dispatch_get_main_queue(), ^{
741  _programmaticallyTransitioningFullScreen = true;
742  [self toggleFullScreen:nil];
743  self.styleMask = _styleMaskWhenWindowed;
744  [self updateFullScreenMenu];
745  });
746  });
747  }
748  else
749  {
750  [self setFullScreenPresentation:NO];
751 
752  NSSize car = self.contentAspectRatio;
753  self.styleMask = _styleMaskWhenWindowed;
754  self.title = _titleBackup;
755  if (!NSEqualSizes(car, NSMakeSize(0,0)))
756  self.contentAspectRatio = car;
757 
758  [self setFrame:[self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
759 
760  [self finishFullScreenTransition];
761  [self updateFullScreenMenu];
762  }
763  }
764 }
765 
772 {
773  _programmaticallyResizingWindow = false;
774 
775  [self.delegate windowDidResize:nil];
776 
777  [self makeFirstResponder:self];
778 
779  if (self.shouldGoFullscreen)
780  dispatch_async(dispatch_get_main_queue(), ^{
781  NSScreen *s = [self.shouldGoFullscreen retain];
782  self.shouldGoFullscreen = nil;
783  [self setFullScreen:true onScreen:s];
784  [s release];
785  });
786 }
787 
793 - (void)toggleFullScreen
794 {
795  [self setFullScreen:![self isFullScreen] onScreen:nil];
796 }
797 
803 - (void)toggleRecording
804 {
805  if (!self.recorder)
806  {
807  // Start recording to a temporary file (rather than first asking for the filename, to make it quicker to start recording).
808  self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
809  _recorder = [[VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
810 
811  [self updateUI];
812  }
813  else
814  [self stopRecording];
815 }
816 
820 - (void)promptToSaveMovie
821 {
822  NSSavePanel *sp = [NSSavePanel savePanel];
823  [sp setTitle:@"Save Movie"];
824  [sp setNameFieldLabel:@"Save Movie To:"];
825  [sp setPrompt:@"Save"];
826  [sp setAllowedFileTypes:@[@"mov"]];
827 
828  char *title = VuoApp_getName();
829  sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
830  free(title);
831 
832  if ([sp runModal] == NSFileHandlingPanelCancelButton)
833  goto done;
834 
835  NSError *error;
836  if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
837  {
838  if ([error code] == NSFileWriteFileExistsError)
839  {
840  // File exists. Since, in the NSSavePanel, the user said to Replace, try replacing it.
841  if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
842  withItemAtURL:_temporaryMovieURL
843  backupItemName:nil
844  options:0
845  resultingItemURL:nil
846  error:&error])
847  {
848  // Replacement failed; show error…
849  NSAlert *alert = [NSAlert alertWithError:error];
850  [alert runModal];
851 
852  // …and give the user another chance.
853  [self promptToSaveMovie];
854  }
855  goto done;
856  }
857 
858  NSAlert *alert = [NSAlert alertWithError:error];
859  [alert runModal];
860  }
861 
862 done:
863  self.temporaryMovieURL = nil;
864 }
865 
873 - (void)stopRecording
874 {
875  if (!_recorder)
876  return;
877 
878  [_recorder finish];
879  [_recorder release];
880  _recorder = nil;
881 
882  [self updateUI];
883 
884  [self setFullScreen:NO onScreen:nil];
885 
886  [self promptToSaveMovie];
887 }
888 
894 - (void)cancelOperation:(id)sender
895 {
896  [self setFullScreen:NO onScreen:nil];
897 }
898 
902 - (void)close
903 {
904  NSView *v = self.contentView;
905  VuoGraphicsLayer *gv = (VuoGraphicsLayer *)v.layer;
906  [gv close];
907 
908  [super close];
909 }
910 
914 - (void)dealloc
915 {
916  // https://b33p.net/kosada/node/12123
917  // Cocoa leaks the contentView unless we force it to resign firstResponder status.
918  [self makeFirstResponder:nil];
919 
920  [super dealloc];
921 }
922 
923 @end