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