18 #import <Carbon/Carbon.h>
22 "title" :
"VuoGraphicsWindow",
26 "VuoGraphicsWindowDelegate",
32 "VuoList_VuoWindowProperty"
50 static void __attribute__((constructor)) VuoGraphicsWindow_init()
60 @property(retain) NSMenuItem *recordMenuItem;
61 @property(retain) id<NSWindowDelegate> privateDelegate;
63 @property(retain) NSScreen *shouldGoFullscreen;
67 @property NSSize pendingResize;
79 userData:(
void *)userData
81 NSRect mainScreenFrame = [[NSScreen mainScreen] frame];
83 _styleMaskWhenWindowed = NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
84 if (
self = [super initWithContentRect:_contentRectWhenWindowed
85 styleMask:_styleMaskWhenWindowed
86 backing:NSBackingStoreBuffered
89 self.colorSpace = NSColorSpace.sRGBColorSpace;
92 self.delegate =
self.privateDelegate;
93 self.releasedWhenClosed = NO;
95 _cursor = VuoCursor_Pointer;
99 self.acceptsMouseMovedEvents = YES;
101 self.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
104 #pragma clang diagnostic push
105 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
107 [
self registerForDraggedTypes:@[NSFilenamesPboardType]];
108 #pragma clang diagnostic pop
110 _userResizedWindow = NO;
111 _programmaticallyResizingWindow = NO;
112 _constrainToScreen = YES;
115 self.title = [NSString stringWithUTF8String:title];
118 _backingScaleFactorCached =
self.backingScaleFactor;
121 initCallback:initCallback
122 updateBackingCallback:updateBackingCallback
123 backingScaleFactor:_backingScaleFactorCached
124 resizeCallback:resizeCallback
125 drawCallback:drawCallback
127 l.contentsScale = _backingScaleFactorCached;
133 self.contentView = v;
136 _contentRectCached = l.frame;
138 _mouseMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown|NSEventMaskLeftMouseUp handler:^(NSEvent *event) {
139 if (event.type == NSEventTypeLeftMouseDown)
140 _leftMouseDown = true;
141 else if (event.type == NSEventTypeLeftMouseUp)
143 _leftMouseDown = false;
144 if (!NSEqualSizes(_pendingResize, NSZeroSize))
146 VDebugLog("The window drag has concluded; performing the deferred resize.");
147 self.contentSize = _pendingResize;
148 _pendingResize = NSZeroSize;
168 NSView *v =
self.contentView;
177 - (BOOL)canBecomeMainWindow
185 - (BOOL)canBecomeKeyWindow
190 - (void)updateFullScreenMenu
192 NSMenuItem *fullScreenMenuItem = [[[[[NSApplication sharedApplication] mainMenu] itemWithTag:VuoViewMenuItemTag] submenu] itemWithTag:VuoFullScreenMenuItemTag];
193 fullScreenMenuItem.title =
self.isFullScreen ?
@"Exit Full Screen" :
@"Enter Full Screen";
201 - (void)becomeMainWindow
203 [
super becomeMainWindow];
205 if (!_isInMacFullScreenMode)
206 [
self setFullScreenPresentation:self.isFullScreen];
208 NSMenu *fileMenu = [[[NSMenu alloc] initWithTitle:@"File"] autorelease];
209 _recordMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleRecording) keyEquivalent:@"e"];
210 [_recordMenuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand|NSEventModifierFlagOption];
211 [fileMenu addItem:_recordMenuItem];
212 NSMenuItem *fileMenuItem = [[NSMenuItem new] autorelease];
213 [fileMenuItem setSubmenu:fileMenu];
215 NSMenu *viewMenu = [[[NSMenu alloc] initWithTitle:@"View"] autorelease];
216 NSMenuItem *fullScreenMenuItem = [[[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleFullScreen) keyEquivalent:@"f"] autorelease];
218 [viewMenu addItem:fullScreenMenuItem];
219 NSMenuItem *viewMenuItem = [[NSMenuItem new] autorelease];
221 [viewMenuItem setSubmenu:viewMenu];
223 NSMenu *windowMenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
224 NSMenuItem *minimizeMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease];
225 NSMenuItem *zoomMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""] autorelease];
226 NSMenuItem *cycleMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Cycle Through Windows" action:@selector(_cycleWindows:) keyEquivalent:@"`"] autorelease];
227 [windowMenu addItem:minimizeMenuItem];
228 [windowMenu addItem:zoomMenuItem];
229 [windowMenu addItem:cycleMenuItem];
230 NSMenuItem *windowMenuItem = [[NSMenuItem new] autorelease];
231 [windowMenuItem setSubmenu:windowMenu];
233 NSMutableArray *windowMenuItems = [NSMutableArray arrayWithCapacity:3];
234 [windowMenuItems addObject:fileMenuItem];
235 [windowMenuItems addObject:viewMenuItem];
236 [windowMenuItems addObject:windowMenuItem];
239 [
self updateFullScreenMenu];
247 - (void)resignMainWindow
249 [
super resignMainWindow];
268 - (void)keyDown:(NSEvent *)event
270 if ([event keyCode] == kVK_Escape && [
self isFullScreen])
271 [
super keyDown:event];
280 _recordMenuItem.title =
@"Stop Recording…";
282 _recordMenuItem.title =
@"Start Recording";
300 NSView *v =
self.contentView;
321 NSView *v =
self.contentView;
336 NSView *v =
self.contentView;
341 _updatedWindow = NULL;
342 _showedWindow = NULL;
343 _requestedFrame = NULL;
353 __block NSUInteger styleMask;
355 styleMask =
self.styleMask;
357 return _isInMacFullScreenMode || styleMask == 0;
364 - (void)setTitle:(NSString *)title
366 self.titleBackup = title;
378 for (
unsigned int i = 1; i <= propertyCount; ++i)
383 if (property.type == VuoWindowProperty_Title)
385 self.title =
property.title ? [NSString stringWithUTF8String:property.title] :
@"";
393 else if (property.type == VuoWindowProperty_FullScreen)
398 bool wantsFullScreen =
property.fullScreen;
402 if (wantsFullScreen && !requestedScreen)
405 NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
406 NSInteger currentDeviceId = [[[
self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
407 bool changingScreen = requestedDeviceId != currentDeviceId;
413 [
self setFullScreen:NO onScreen:nil];
416 if (NSScreen.screensHaveSeparateSpaces)
418 self.shouldGoFullscreen = requestedScreen;
421 [
self setFullScreen:YES onScreen:requestedScreen];
424 [
self setFullScreen:property.fullScreen onScreen:requestedScreen];
427 [
self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
428 NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
430 else if (property.type == VuoWindowProperty_Position)
432 NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
433 if (property.unit == VuoCoordinateUnit_Pixels)
434 propertyInPoints = [
self.contentView convertRectFromBacking:propertyInPoints];
436 NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
438 _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
441 NSRect contentRect = [
self contentRectForFrameRect:[
self frame]];
442 self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
445 else if (property.type == VuoWindowProperty_Size)
447 NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
448 if (property.unit == VuoCoordinateUnit_Pixels)
449 propertyInPoints = [
self.contentView convertRectFromBacking:propertyInPoints];
450 _maintainsPixelSizeWhenBackingChanges = (
property.unit == VuoCoordinateUnit_Pixels);
453 _contentRectWhenWindowed.size = propertyInPoints.size;
456 NSRect contentRect = [
self contentRectForFrameRect:[
self frame]];
460 contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
462 contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
465 _constrainToScreen = NO;
466 [
self setFrame:[
self frameRectForContentRect:contentRect] display:YES animate:NO];
467 _constrainToScreen = YES;
469 @catch (NSException *e)
473 VUserLog(
"Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
477 else if (property.type == VuoWindowProperty_AspectRatio)
479 if (property.aspectRatio < 1./10000
480 || property.aspectRatio > 10000)
482 VUserLog(
"Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
486 NSRect contentRect = [
self contentRectForFrameRect:[
self frame]];
487 [
self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
489 else if (property.type == VuoWindowProperty_AspectRatioReset)
490 [
self unlockAspectRatio];
491 else if (property.type == VuoWindowProperty_Resizable)
493 [[
self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
495 _styleMaskWhenWindowed =
property.resizable ? (_styleMaskWhenWindowed | NSWindowStyleMaskResizable) : (_styleMaskWhenWindowed & ~NSWindowStyleMaskResizable);
497 self.styleMask =
property.resizable ? ([
self styleMask] | NSWindowStyleMaskResizable) : ([
self styleMask] & ~NSWindowStyleMaskResizable);
499 else if (property.type == VuoWindowProperty_Cursor)
501 _cursor =
property.cursor;
502 [
self invalidateCursorRectsForView:self.contentView];
504 else if (property.type == VuoWindowProperty_Level)
506 if (property.level == VuoWindowLevel_Background)
507 self.level = CGWindowLevelForKey(kCGBackstopMenuLevelKey);
508 else if (property.level == VuoWindowLevel_Normal)
509 self.level = CGWindowLevelForKey(kCGNormalWindowLevelKey);
510 else if (property.level == VuoWindowLevel_Floating)
511 self.level = CGWindowLevelForKey(kCGFloatingWindowLevelKey);
525 extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
527 if (GetWindowBounds(
self.windowRef, kWindowStructureRgn, &qdRect) == noErr)
528 return NSMakeRect(qdRect.left,
529 (
float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
530 qdRect.right - qdRect.left,
531 qdRect.bottom - qdRect.top);
547 - (void)setAspectRatioToWidth:(
unsigned int)pixelsWide height:(
unsigned int)pixelsHigh
552 NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
553 if (NSEqualSizes([
self contentAspectRatio], newAspect))
557 self.contentAspectRatio = newAspect;
572 CGFloat desiredWidth = pixelsWide;
573 CGFloat desiredHeight = pixelsHigh;
574 CGRect windowFrame = [
self liveFrame];
575 CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
578 if (_userResizedWindow)
581 desiredWidth = CGRectGetWidth(windowFrame);
582 desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
586 NSRect screenFrame = [[
self screen] visibleFrame];
587 NSRect maxContentRect = [
self contentRectForFrameRect:screenFrame];
588 if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
590 CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
591 if (aspectRatio >= maxContentAspectRatio)
594 desiredWidth = maxContentRect.size.width;
595 desiredHeight = maxContentRect.size.width / aspectRatio;
600 desiredWidth = maxContentRect.size.height * aspectRatio;
601 desiredHeight = maxContentRect.size.height;
605 NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
606 NSSize newWindowSize = [
self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
609 CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
610 NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
612 _programmaticallyResizingWindow = YES;
613 [
self setFrame:newWindowFrame display:YES];
614 _programmaticallyResizingWindow = NO;
633 self.resizeIncrements = NSMakeSize(1,1);
640 - (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
642 if (_constrainToScreen)
643 return [
super constrainFrameRect:frameRect toScreen:screen];
651 - (void)setFullScreenPresentation:(
bool)enabled
659 ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
664 ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
704 - (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
706 _programmaticallyResizingWindow =
true;
714 _contentRectWhenWindowed = [
self contentRectForFrameRect:self.frame];
715 _styleMaskWhenWindowed = [
self styleMask];
719 if (screen && ![[
self screen] isEqualTo:screen])
721 NSRect windowFrame =
self.frame;
722 NSRect screenFrame = screen.frame;
723 self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
724 screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
731 bool useMacFullScreenMode =
true;
735 if (!NSScreen.screensHaveSeparateSpaces)
736 useMacFullScreenMode =
false;
739 if (useMacFullScreenMode)
741 _isInMacFullScreenMode = YES;
746 self.styleMask |= NSWindowStyleMaskResizable;
748 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
750 dispatch_async(dispatch_get_main_queue(), ^{
751 _programmaticallyTransitioningFullScreen =
true;
752 [
self toggleFullScreen:nil];
753 [
self updateFullScreenMenu];
759 [
self setFullScreenPresentation:YES];
761 NSSize car =
self.contentAspectRatio;
763 if (!NSEqualSizes(car, NSMakeSize(0,0)))
764 self.contentAspectRatio = car;
767 [
self setFrame:self.screen.frame display:YES];
769 [
self finishFullScreenTransition];
770 [
self updateFullScreenMenu];
777 if (_isInMacFullScreenMode)
779 _isInMacFullScreenMode = NO;
781 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
783 dispatch_async(dispatch_get_main_queue(), ^{
784 _programmaticallyTransitioningFullScreen =
true;
785 [
self toggleFullScreen:nil];
786 self.styleMask = _styleMaskWhenWindowed;
787 [
self updateFullScreenMenu];
793 [
self setFullScreenPresentation:NO];
795 NSSize car =
self.contentAspectRatio;
796 self.styleMask = _styleMaskWhenWindowed;
797 self.title = _titleBackup;
798 if (!NSEqualSizes(car, NSMakeSize(0,0)))
799 self.contentAspectRatio = car;
801 [
self setFrame:[
self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
803 [
self finishFullScreenTransition];
804 [
self updateFullScreenMenu];
816 _programmaticallyResizingWindow =
false;
818 NSNotification *n = [NSNotification notificationWithName:@"unused" object:nil];
819 [
self.delegate windowDidResize:n];
821 [
self makeFirstResponder:self];
823 if (
self.shouldGoFullscreen)
824 dispatch_async(dispatch_get_main_queue(), ^{
825 NSScreen *s = [
self.shouldGoFullscreen retain];
826 self.shouldGoFullscreen = nil;
827 [
self setFullScreen:true onScreen:s];
837 - (void)toggleFullScreen
839 [
self setFullScreen:![
self isFullScreen] onScreen:nil];
846 - (void)scheduleResize:(NSSize)newSize
848 VDebugLog(
"Requested to resize to %gx%g points.",newSize.width,newSize.height);
851 VDebugLog(
" The window is being dragged; deferring the resize.");
852 _pendingResize = newSize;
856 VDebugLog(
" The window is not being dragged; resizing now.");
857 self.contentSize = newSize;
866 - (void)toggleRecording
871 self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
872 _recorder = [[
VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
877 [
self stopRecording];
883 - (void)promptToSaveMovie
885 NSSavePanel *sp = [NSSavePanel savePanel];
886 [sp setTitle:@"Save Movie"];
887 [sp setNameFieldLabel:@"Save Movie To:"];
888 [sp setPrompt:@"Save"];
889 #pragma clang diagnostic push
890 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
892 [sp setAllowedFileTypes:@[@"mov"]];
893 #pragma clang diagnostic pop
896 sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
899 if ([sp runModal] == NSModalResponseCancel)
903 if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
905 if ([error code] == NSFileWriteFileExistsError)
908 if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
909 withItemAtURL:_temporaryMovieURL
916 NSAlert *alert = [NSAlert alertWithError:error];
920 [
self promptToSaveMovie];
925 NSAlert *alert = [NSAlert alertWithError:error];
930 self.temporaryMovieURL = nil;
940 - (void)stopRecording
951 [
self setFullScreen:NO onScreen:nil];
953 [
self promptToSaveMovie];
961 - (void)cancelOperation:(
id)sender
963 [
self setFullScreen:NO onScreen:nil];
971 NSView *v =
self.contentView;
983 [NSEvent removeMonitor:_mouseMonitor];
987 [
self makeFirstResponder:nil];