Vuo 2.4.4
Loading...
Searching...
No Matches
VuoGraphicsWindow.m
Go to the documentation of this file.
1
10#import "VuoApp.h"
11#import "VuoGraphicsLayer.h"
12#import "VuoGraphicsView.h"
13#import "VuoGraphicsWindow.h"
16#import "VuoScreenCommon.h"
17#import <Carbon/Carbon.h>
18
19#ifdef VUO_COMPILER
21 "title" : "VuoGraphicsWindow",
22 "dependencies" : [
23 "VuoGraphicsLayer",
24 "VuoGraphicsView",
25 "VuoGraphicsWindowDelegate",
26 "VuoRenderedLayers",
27 "VuoScreenCommon",
28 "VuoWindowProperty",
29 "VuoWindowRecorder",
30 "VuoWindowReference",
31 "VuoList_VuoWindowProperty"
32 ]
33});
34#endif
35
39
40const NSInteger VuoViewMenuItemTag = 1000;
41const NSInteger VuoFullScreenMenuItemTag = 1001;
42
45
49static void __attribute__((constructor)) VuoGraphicsWindow_init()
50{
51 VuoGraphicsWindow_fullScreenTransitionSemaphore = dispatch_semaphore_create(1);
52}
53
54
58@interface VuoGraphicsWindow ()
59@property(retain) NSMenuItem *recordMenuItem;
60@property(retain) id<NSWindowDelegate> privateDelegate;
61@property bool constrainToScreen;
62@property(retain) NSScreen *shouldGoFullscreen;
63
64@property bool leftMouseDown;
65@property id mouseMonitor;
66@property NSSize pendingResize;
67@end
68
69@implementation VuoGraphicsWindow
70
74- (instancetype)initWithInitCallback:(VuoGraphicsWindowInitCallback)initCallback
75 updateBackingCallback:(VuoGraphicsWindowUpdateBackingCallback)updateBackingCallback
76 resizeCallback:(VuoGraphicsWindowResizeCallback)resizeCallback
77 drawCallback:(VuoGraphicsWindowDrawCallback)drawCallback
78 userData:(void *)userData
79{
80 NSRect mainScreenFrame = [[NSScreen mainScreen] frame];
81 _contentRectWhenWindowed = NSMakeRect(mainScreenFrame.origin.x, mainScreenFrame.origin.y, VuoGraphicsWindowDefaultWidth, 768);
82 _styleMaskWhenWindowed = NSWindowStyleMaskTitled | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable;
83 if (self = [super initWithContentRect:_contentRectWhenWindowed
84 styleMask:_styleMaskWhenWindowed
85 backing:NSBackingStoreBuffered
86 defer:NO])
87 {
88 self.colorSpace = NSColorSpace.sRGBColorSpace;
89
90 self.privateDelegate = [[[VuoGraphicsWindowDelegate alloc] initWithWindow:self] autorelease];
91 self.delegate = self.privateDelegate;
92 self.releasedWhenClosed = NO;
93
94 _cursor = VuoCursor_Pointer;
95
96 self.contentMinSize = NSMakeSize(VuoGraphicsWindowMinSize, VuoGraphicsWindowMinSize);
97
98 self.acceptsMouseMovedEvents = YES;
99
100 self.collectionBehavior = NSWindowCollectionBehaviorFullScreenPrimary;
101
102 [self initDrag];
103#pragma clang diagnostic push
104#pragma clang diagnostic ignored "-Wdeprecated-declarations"
105 // The replacement, NSPasteboardTypeFileURL, isn't available until macOS 11.
106 [self registerForDraggedTypes:@[NSFilenamesPboardType]];
107#pragma clang diagnostic pop
108
109 _userResizedWindow = NO;
110 _programmaticallyResizingWindow = NO;
111 _constrainToScreen = YES;
112
113 char *title = VuoApp_getName();
114 self.title = [NSString stringWithUTF8String:title];
115 free(title);
116
117 _backingScaleFactorCached = self.backingScaleFactor;
118
119 VuoGraphicsLayer *l = [[VuoGraphicsLayer alloc] initWithWindow:self
120 initCallback:initCallback
121 updateBackingCallback:updateBackingCallback
122 backingScaleFactor:_backingScaleFactorCached
123 resizeCallback:resizeCallback
124 drawCallback:drawCallback
125 userData:userData];
126 l.contentsScale = _backingScaleFactorCached;
127
128 VuoGraphicsView *v = [[VuoGraphicsView alloc] init];
129 v.layer = l;
130 [l release];
131
132 self.contentView = v;
133 [v release];
134
135 _contentRectCached = l.frame;
136
137 _mouseMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown|NSEventMaskLeftMouseUp handler:^(NSEvent *event) {
138 if (event.type == NSEventTypeLeftMouseDown)
139 _leftMouseDown = true;
140 else if (event.type == NSEventTypeLeftMouseUp)
141 {
142 _leftMouseDown = false;
143 if (!NSEqualSizes(_pendingResize, NSZeroSize))
144 {
145 VDebugLog("The window drag has concluded; performing the deferred resize.");
146 self.contentSize = _pendingResize;
147 _pendingResize = NSZeroSize;
148 }
149 }
150 return event;
151 }];
152 }
153
154 return self;
155}
156
163- (void)draw
164{
165 __block VuoGraphicsLayer *l;
167 NSView *v = self.contentView;
168 l = (VuoGraphicsLayer *)v.layer;
169 });
170 [l draw];
171}
172
176- (BOOL)canBecomeMainWindow
177{
178 return YES;
179}
180
184- (BOOL)canBecomeKeyWindow
185{
186 return YES;
187}
188
189- (void)updateFullScreenMenu
190{
191 NSMenuItem *fullScreenMenuItem = [[[[[NSApplication sharedApplication] mainMenu] itemWithTag:VuoViewMenuItemTag] submenu] itemWithTag:VuoFullScreenMenuItemTag];
192 fullScreenMenuItem.title = self.isFullScreen ? @"Exit Full Screen" : @"Enter Full Screen";
193}
194
200- (void)becomeMainWindow
201{
202 [super becomeMainWindow];
203
204 if (!_isInMacFullScreenMode)
205 [self setFullScreenPresentation:self.isFullScreen];
206
207 NSMenu *fileMenu = [[[NSMenu alloc] initWithTitle:@"File"] autorelease];
208 _recordMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleRecording) keyEquivalent:@"e"];
209 [_recordMenuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand|NSEventModifierFlagOption];
210 [fileMenu addItem:_recordMenuItem];
211 NSMenuItem *fileMenuItem = [[NSMenuItem new] autorelease];
212 [fileMenuItem setSubmenu:fileMenu];
213
214 NSMenu *viewMenu = [[[NSMenu alloc] initWithTitle:@"View"] autorelease];
215 NSMenuItem *fullScreenMenuItem = [[[NSMenuItem alloc] initWithTitle:@"" action:@selector(toggleFullScreen) keyEquivalent:@"f"] autorelease];
216 fullScreenMenuItem.tag = VuoFullScreenMenuItemTag;
217 [viewMenu addItem:fullScreenMenuItem];
218 NSMenuItem *viewMenuItem = [[NSMenuItem new] autorelease];
219 viewMenuItem.tag = VuoViewMenuItemTag;
220 [viewMenuItem setSubmenu:viewMenu];
221
222 NSMenu *windowMenu = [[[NSMenu alloc] initWithTitle:@"Window"] autorelease];
223 NSMenuItem *minimizeMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"] autorelease];
224 NSMenuItem *zoomMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""] autorelease];
225 NSMenuItem *cycleMenuItem = [[[NSMenuItem alloc] initWithTitle:@"Cycle Through Windows" action:@selector(_cycleWindows:) keyEquivalent:@"`"] autorelease];
226 [windowMenu addItem:minimizeMenuItem];
227 [windowMenu addItem:zoomMenuItem];
228 [windowMenu addItem:cycleMenuItem];
229 NSMenuItem *windowMenuItem = [[NSMenuItem new] autorelease];
230 [windowMenuItem setSubmenu:windowMenu];
231
232 NSMutableArray *windowMenuItems = [NSMutableArray arrayWithCapacity:3];
233 [windowMenuItems addObject:fileMenuItem];
234 [windowMenuItems addObject:viewMenuItem];
235 [windowMenuItems addObject:windowMenuItem];
236 self.oldMenu = (NSMenu *)VuoApp_setMenuItems(windowMenuItems);
237
238 [self updateFullScreenMenu];
239
240 [self updateUI];
241}
242
246- (void)resignMainWindow
247{
248 [super resignMainWindow];
249
250 // No need to change presentation when resigning, since:
251 // - other VuoGraphicsWindows change presentation on becomeMainWindow
252 // - Mac OS X changes presentation when another app becomes active
253 //
254 // Also, if two VuoGraphicsWindows are fullscreen on separate displays,
255 // switching out of fullscreen presentation will cause the menubar and dock to flicker
256 // when clicking from one display to another.
257// if (!_isInMacFullScreenMode)
258// [self setFullScreenPresentation:NO];
259
260 VuoApp_setMenu(_oldMenu);
261 self.oldMenu = nil;
262}
263
267- (void)keyDown:(NSEvent *)event
268{
269 if ([event keyCode] == kVK_Escape && [self isFullScreen])
270 [super keyDown:event];
271}
272
276- (void)updateUI
277{
278 if (_recorder)
279 _recordMenuItem.title = @"Stop Recording…";
280 else
281 _recordMenuItem.title = @"Start Recording";
282}
283
289- (void)enableUpdatedWindowTrigger:(VuoGraphicsWindowUpdatedWindowCallback)updatedWindow
290{
291 _updatedWindow = updatedWindow;
292
295 _updatedWindow(rl);
296
297 __block VuoGraphicsLayer *l;
299 NSView *v = self.contentView;
300 l = (VuoGraphicsLayer *)v.layer;
301 });
302 [l enableTriggers];
303}
304
311- (void)enableShowedWindowTrigger:(VuoGraphicsWindowShowedWindowCallback)showedWindow requestedFrameTrigger:(VuoGraphicsWindowRequestedFrameCallback)requestedFrame
312{
313 _showedWindow = showedWindow;
314 _showedWindow(VuoWindowReference_make(self));
315
316 _requestedFrame = requestedFrame;
317
318 __block VuoGraphicsLayer *l;
320 NSView *v = self.contentView;
321 l = (VuoGraphicsLayer *)v.layer;
322 });
323 [l enableTriggers];
324}
325
331- (void)disableTriggers
332{
333 __block VuoGraphicsLayer *l;
335 NSView *v = self.contentView;
336 l = (VuoGraphicsLayer *)v.layer;
337 });
338 [l disableTriggers];
339
340 _updatedWindow = NULL;
341 _showedWindow = NULL;
342 _requestedFrame = NULL;
343}
344
350- (bool)isFullScreen
351{
352 __block NSUInteger styleMask;
354 styleMask = self.styleMask;
355 });
356 return _isInMacFullScreenMode || styleMask == 0;
357}
358
359
363- (void)setTitle:(NSString *)title
364{
365 self.titleBackup = title;
366 super.title = title;
367}
368
374- (void)setProperties:(VuoList_VuoWindowProperty)properties
375{
376 unsigned int propertyCount = VuoListGetCount_VuoWindowProperty(properties);
377 for (unsigned int i = 1; i <= propertyCount; ++i)
378 {
381
382 if (property.type == VuoWindowProperty_Title)
383 {
384 self.title = property.title ? [NSString stringWithUTF8String:property.title] : @"";
385 if (_updatedWindow)
386 {
389 _updatedWindow(rl);
390 }
391 }
392 else if (property.type == VuoWindowProperty_FullScreen)
393 {
394 NSScreen *requestedScreen = VuoScreen_getNSScreen(property.screen);
395
396 bool isFullScreen = self.isFullScreen;
397 bool wantsFullScreen = property.fullScreen;
398
399 // Only go fullscreen if the specific requested screen exists.
400 // https://b33p.net/kosada/node/14658
401 if (wantsFullScreen && !requestedScreen)
402 continue;
403
404 NSInteger requestedDeviceId = [[[requestedScreen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
405 NSInteger currentDeviceId = [[[self.screen deviceDescription] objectForKey:@"NSScreenNumber"] integerValue];
406 bool changingScreen = requestedDeviceId != currentDeviceId;
407
408 if (isFullScreen && wantsFullScreen && changingScreen)
409 {
410 // Temporarily switch back to windowed mode so that we can switch to fullscreen on the new screen
411 // (since macOS doesn't let us directly move from one fullscreen to another).
412 [self setFullScreen:NO onScreen:nil];
413
414 // If `System Settings > Desktop & Dock > Displays have separate Spaces` is enabled…
415 if (NSScreen.screensHaveSeparateSpaces)
416 // Give macOS a chance to complete its exit-fullscreen transition.
417 self.shouldGoFullscreen = requestedScreen;
418 else
419 // If not, we can immediately go fullscreen on the new screen.
420 [self setFullScreen:YES onScreen:requestedScreen];
421 }
422 else if (isFullScreen != wantsFullScreen)
423 [self setFullScreen:property.fullScreen onScreen:requestedScreen];
424 else if (requestedScreen && !isFullScreen)
425 // Move the non-fullscreen window to the center of the specified screen.
426 [self setFrameOrigin:(NSPoint){ NSMidX(requestedScreen.visibleFrame) - self.frame.size.width / 2,
427 NSMidY(requestedScreen.visibleFrame) - self.frame.size.height /2 }];
428 }
429 else if (property.type == VuoWindowProperty_Position)
430 {
431 NSRect propertyInPoints = NSMakeRect(property.left, property.top, 0, 0);
432 if (property.unit == VuoCoordinateUnit_Pixels)
433 propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
434
435 NSRect mainScreenRect = [[[NSScreen screens] objectAtIndex:0] frame];
436 if ([self isFullScreen])
437 _contentRectWhenWindowed.origin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - _contentRectWhenWindowed.size.height - propertyInPoints.origin.y);
438 else
439 {
440 NSRect contentRect = [self contentRectForFrameRect:[self frame]];
441 self.frameOrigin = NSMakePoint(propertyInPoints.origin.x, mainScreenRect.size.height - contentRect.size.height - propertyInPoints.origin.y);
442 }
443 }
444 else if (property.type == VuoWindowProperty_Size)
445 {
446 NSRect propertyInPoints = NSMakeRect(0, 0, property.width, property.height);
447 if (property.unit == VuoCoordinateUnit_Pixels)
448 propertyInPoints = [self.contentView convertRectFromBacking:propertyInPoints];
449 _maintainsPixelSizeWhenBackingChanges = (property.unit == VuoCoordinateUnit_Pixels);
450
451 if ([self isFullScreen])
452 _contentRectWhenWindowed.size = propertyInPoints.size;
453 else
454 {
455 NSRect contentRect = [self contentRectForFrameRect:[self frame]];
456
457 // Adjust the y position by the change in height, so that the window appears to be anchored in its top-left corner
458 // (instead of its bottom-left corner as the system does by default).
459 contentRect.origin.y += contentRect.size.height - propertyInPoints.size.height;
460
461 contentRect.size = NSMakeSize(propertyInPoints.size.width, propertyInPoints.size.height);
462 @try
463 {
464 _constrainToScreen = NO;
465 [self setFrame:[self frameRectForContentRect:contentRect] display:YES animate:NO];
466 _constrainToScreen = YES;
467 }
468 @catch (NSException *e)
469 {
470 VuoText description = VuoText_makeFromCFString(e.description);
471 VuoLocal(description);
472 VUserLog("Error: Couldn't change window size to %lldx%lld: %s", property.width, property.height, description);
473 }
474 }
475 }
476 else if (property.type == VuoWindowProperty_AspectRatio)
477 {
478 if (property.aspectRatio < 1./10000
479 || property.aspectRatio > 10000)
480 {
481 VUserLog("Error: Couldn't change window aspect ratio to %g since it's unrealistically narrow.", property.aspectRatio);
482 continue;
483 }
484
485 NSRect contentRect = [self contentRectForFrameRect:[self frame]];
486 [self setAspectRatioToWidth:contentRect.size.width height:contentRect.size.width/property.aspectRatio];
487 }
488 else if (property.type == VuoWindowProperty_AspectRatioReset)
489 [self unlockAspectRatio];
490 else if (property.type == VuoWindowProperty_Resizable)
491 {
492 [[self standardWindowButton:NSWindowZoomButton] setEnabled:property.resizable];
493 if ([self isFullScreen])
494 _styleMaskWhenWindowed = property.resizable ? (_styleMaskWhenWindowed | NSWindowStyleMaskResizable) : (_styleMaskWhenWindowed & ~NSWindowStyleMaskResizable);
495 else
496 self.styleMask = property.resizable ? ([self styleMask] | NSWindowStyleMaskResizable) : ([self styleMask] & ~NSWindowStyleMaskResizable);
497 }
498 else if (property.type == VuoWindowProperty_Cursor)
499 {
500 _cursor = property.cursor;
501 [self invalidateCursorRectsForView:self.contentView];
502 }
503 else if (property.type == VuoWindowProperty_Level)
504 {
505 if (property.level == VuoWindowLevel_Background)
506 self.level = CGWindowLevelForKey(kCGBackstopMenuLevelKey);
507 else if (property.level == VuoWindowLevel_Normal)
508 self.level = CGWindowLevelForKey(kCGNormalWindowLevelKey);
509 else if (property.level == VuoWindowLevel_Floating)
510 self.level = CGWindowLevelForKey(kCGFloatingWindowLevelKey);
511 }
512 }
513}
514
521- (NSRect)liveFrame
522{
523 Rect qdRect;
524 extern OSStatus GetWindowBounds(WindowRef window, WindowRegionCode regionCode, Rect *globalBounds);
525
526 if (GetWindowBounds(self.windowRef, kWindowStructureRgn, &qdRect) == noErr)
527 return NSMakeRect(qdRect.left,
528 (float)CGDisplayPixelsHigh(kCGDirectMainDisplay) - qdRect.bottom,
529 qdRect.right - qdRect.left,
530 qdRect.bottom - qdRect.top);
531 else
532 // The above GetWindowBounds API seems to always return `paramErr` on macOS 11.
533 return self.frame;
534}
535
546- (void)setAspectRatioToWidth:(unsigned int)pixelsWide height:(unsigned int)pixelsHigh
547{
548 pixelsWide = MAX(VuoGraphicsWindowMinSize, pixelsWide);
549 pixelsHigh = MAX(VuoGraphicsWindowMinSize, pixelsHigh);
550
551 NSSize newAspect = NSMakeSize(pixelsWide, pixelsHigh);
552 if (NSEqualSizes([self contentAspectRatio], newAspect))
553 return;
554
555 // Sets the constraint when the user resizes the window (but doesn't affect the window's current size).
556 self.contentAspectRatio = newAspect;
557
558 if ([self isFullScreen])
559 {
560 if (_updatedWindow)
561 {
564 _updatedWindow(rl);
565 }
566 if (_showedWindow)
567 _showedWindow(VuoWindowReference_make(self));
568 return;
569 }
570
571 CGFloat desiredWidth = pixelsWide;
572 CGFloat desiredHeight = pixelsHigh;
573 CGRect windowFrame = [self liveFrame];
574 CGFloat aspectRatio = (CGFloat)pixelsWide / (CGFloat)pixelsHigh;
575
576 // Adjust the width and height if the user manually resized the window.
577 if (_userResizedWindow)
578 {
579 // Preserve the width, scale the height.
580 desiredWidth = CGRectGetWidth(windowFrame);
581 desiredHeight = CGRectGetWidth(windowFrame) / aspectRatio;
582 }
583
584 // Adjust the width and height if they don't fit the screen.
585 NSRect screenFrame = [[self screen] visibleFrame];
586 NSRect maxContentRect = [self contentRectForFrameRect:screenFrame];
587 if (desiredWidth > maxContentRect.size.width || desiredHeight > maxContentRect.size.height)
588 {
589 CGFloat maxContentAspectRatio = maxContentRect.size.width / maxContentRect.size.height;
590 if (aspectRatio >= maxContentAspectRatio)
591 {
592 // Too wide, so scale it to the maximum horizontal screen space.
593 desiredWidth = maxContentRect.size.width;
594 desiredHeight = maxContentRect.size.width / aspectRatio;
595 }
596 else
597 {
598 // Too tall, so scale it to the maximum vertical screen space.
599 desiredWidth = maxContentRect.size.height * aspectRatio;
600 desiredHeight = maxContentRect.size.height;
601 }
602 }
603
604 NSSize newContentSize = NSMakeSize(desiredWidth, desiredHeight);
605 NSSize newWindowSize = [self frameRectForContentRect:NSMakeRect(0, 0, newContentSize.width, newContentSize.height)].size;
606
607 // Preserve the window's top left corner position. (If you just resize, it preserves the bottom left corner position.)
608 CGFloat topY = CGRectGetMinY(windowFrame) + CGRectGetHeight(windowFrame);
609 NSRect newWindowFrame = NSMakeRect(CGRectGetMinX(windowFrame), topY - newWindowSize.height, newWindowSize.width, newWindowSize.height);
610
611 _programmaticallyResizingWindow = YES;
612 [self setFrame:newWindowFrame display:YES];
613 _programmaticallyResizingWindow = NO;
614
615 if (_updatedWindow)
616 {
619 _updatedWindow(rl);
620 }
621 if (_showedWindow)
622 _showedWindow(VuoWindowReference_make(self));
623}
624
630- (void)unlockAspectRatio
631{
632 self.resizeIncrements = NSMakeSize(1,1);
633}
634
639- (NSRect)constrainFrameRect:(NSRect)frameRect toScreen:(NSScreen *)screen
640{
641 if (_constrainToScreen)
642 return [super constrainFrameRect:frameRect toScreen:screen];
643 else
644 return frameRect;
645}
646
650- (void)setFullScreenPresentation:(bool)enabled
651{
652 if (enabled)
653 {
654 // Don't raise the window level, since the window still covers the screen even when it's not focused.
655// self.level = NSScreenSaverWindowLevel;
656
657 // Instead, just hide the dock and menu bar.
658 ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
659 }
660 else
661 {
662// self.level = NSNormalWindowLevel;
663 ((NSApplication *)NSApp).presentationOptions = NSApplicationPresentationDefault;
664 }
665}
666
703- (void)setFullScreen:(BOOL)wantsFullScreen onScreen:(NSScreen *)screen
704{
705 _programmaticallyResizingWindow = true;
706
707 if (wantsFullScreen && ![self isFullScreen])
708 {
709 // Switch into fullscreen mode.
710
711
712 // Save the position, size, and style, to be restored when switching back to windowed mode.
713 _contentRectWhenWindowed = [self contentRectForFrameRect:self.frame];
714 _styleMaskWhenWindowed = [self styleMask];
715
716 // Move the window to the specified screen.
717 // (Necessary even for Mac-fullscreen mode, since it fullscreens on whichever screen the window is currently on.)
718 if (screen && ![[self screen] isEqualTo:screen])
719 {
720 NSRect windowFrame = self.frame;
721 NSRect screenFrame = screen.frame;
722 self.frameOrigin = NSMakePoint(screenFrame.origin.x + screenFrame.size.width/2 - windowFrame.size.width/2,
723 screenFrame.origin.y + screenFrame.size.height/2 - windowFrame.size.height/2);
724 }
725
726
727 // Use Mac-fullscreen mode by default, since it's compatible with Mission Control,
728 // and since (unlike Kiosk Mode) the menu bar stays hidden on the fullscreen display
729 // when you focus a window on a different display (such as when livecoding during a performance).
730 bool useMacFullScreenMode = true;
731
732 // If `System Settings > Desktop & Dock > Displays have separate Spaces` is unchecked,
733 // only a single window could go Mac-fullscreen at once, so don't use Mac-fullscreen mode in that case.
734 if (!NSScreen.screensHaveSeparateSpaces)
735 useMacFullScreenMode = false;
736
737
738 if (useMacFullScreenMode)
739 {
740 _isInMacFullScreenMode = YES;
741
742 // If a size-locked window enters Mac-fullscreen mode,
743 // its height increases upon exiting Mac-fullscreen mode.
744 // Mark it resizeable while fullscreen, to keep its size from changing (!).
745 self.styleMask |= NSWindowStyleMaskResizable;
746
747 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
748 dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
749 dispatch_async(dispatch_get_main_queue(), ^{
750 _programmaticallyTransitioningFullScreen = true;
751 [self toggleFullScreen:nil];
752 [self updateFullScreenMenu];
753 });
754 });
755 }
756 else
757 {
758 [self setFullScreenPresentation:YES];
759
760 NSSize car = self.contentAspectRatio;
761 self.styleMask = 0;
762 if (!NSEqualSizes(car, NSMakeSize(0,0)))
763 self.contentAspectRatio = car;
764
765 // Make the window take up the whole screen.
766 [self setFrame:self.screen.frame display:YES];
767
768 [self finishFullScreenTransition];
769 [self updateFullScreenMenu];
770 }
771 }
772 else if (!wantsFullScreen && [self isFullScreen])
773 {
774 // Switch out of fullscreen mode.
775
776 if (_isInMacFullScreenMode)
777 {
778 _isInMacFullScreenMode = NO;
779
780 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
781 dispatch_semaphore_wait(VuoGraphicsWindow_fullScreenTransitionSemaphore, DISPATCH_TIME_FOREVER);
782 dispatch_async(dispatch_get_main_queue(), ^{
783 _programmaticallyTransitioningFullScreen = true;
784 [self toggleFullScreen:nil];
785 self.styleMask = _styleMaskWhenWindowed;
786 [self updateFullScreenMenu];
787 });
788 });
789 }
790 else
791 {
792 [self setFullScreenPresentation:NO];
793
794 NSSize car = self.contentAspectRatio;
795 self.styleMask = _styleMaskWhenWindowed;
796 self.title = _titleBackup;
797 if (!NSEqualSizes(car, NSMakeSize(0,0)))
798 self.contentAspectRatio = car;
799
800 [self setFrame:[self frameRectForContentRect:_contentRectWhenWindowed] display:YES];
801
802 [self finishFullScreenTransition];
803 [self updateFullScreenMenu];
804 }
805 }
806}
807
814{
815 _programmaticallyResizingWindow = false;
816
817 NSNotification *n = [NSNotification notificationWithName:@"unused" object:nil];
818 [self.delegate windowDidResize:n];
819
820 [self makeFirstResponder:self];
821
822 if (self.shouldGoFullscreen)
823 dispatch_async(dispatch_get_main_queue(), ^{
824 NSScreen *s = [self.shouldGoFullscreen retain];
825 self.shouldGoFullscreen = nil;
826 [self setFullScreen:true onScreen:s];
827 [s release];
828 });
829}
830
836- (void)toggleFullScreen
837{
838 [self setFullScreen:![self isFullScreen] onScreen:nil];
839}
840
845- (void)scheduleResize:(NSSize)newSize
846{
847 VDebugLog("Requested to resize to %gx%g points.",newSize.width,newSize.height);
848 if (_leftMouseDown)
849 {
850 VDebugLog(" The window is being dragged; deferring the resize.");
851 _pendingResize = newSize;
852 }
853 else
854 {
855 VDebugLog(" The window is not being dragged; resizing now.");
856 self.contentSize = newSize;
857 }
858}
859
865- (void)toggleRecording
866{
867 if (!self.recorder)
868 {
869 // Start recording to a temporary file (rather than first asking for the filename, to make it quicker to start recording).
870 self.temporaryMovieURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]];
871 _recorder = [[VuoWindowRecorder alloc] initWithWindow:self url:_temporaryMovieURL];
872
873 [self updateUI];
874 }
875 else
876 [self stopRecording];
877}
878
882- (void)promptToSaveMovie
883{
884 NSSavePanel *sp = [NSSavePanel savePanel];
885 [sp setTitle:@"Save Movie"];
886 [sp setNameFieldLabel:@"Save Movie To:"];
887 [sp setPrompt:@"Save"];
888#pragma clang diagnostic push
889#pragma clang diagnostic ignored "-Wdeprecated-declarations"
890 // The replacement, setAllowedContentTypes, isn't available until macOS 11.
891 [sp setAllowedFileTypes:@[@"mov"]];
892#pragma clang diagnostic pop
893
894 char *title = VuoApp_getName();
895 sp.nameFieldStringValue = [NSString stringWithUTF8String:title];
896 free(title);
897
898 if ([sp runModal] == NSModalResponseCancel)
899 goto done;
900
901 NSError *error;
902 if (![[NSFileManager defaultManager] moveItemAtURL:_temporaryMovieURL toURL:[sp URL] error:&error])
903 {
904 if ([error code] == NSFileWriteFileExistsError)
905 {
906 // File exists. Since, in the NSSavePanel, the user said to Replace, try replacing it.
907 if (![[NSFileManager defaultManager] replaceItemAtURL:[sp URL]
908 withItemAtURL:_temporaryMovieURL
909 backupItemName:nil
910 options:0
911 resultingItemURL:nil
912 error:&error])
913 {
914 // Replacement failed; show error…
915 NSAlert *alert = [NSAlert alertWithError:error];
916 [alert runModal];
917
918 // …and give the user another chance.
919 [self promptToSaveMovie];
920 }
921 goto done;
922 }
923
924 NSAlert *alert = [NSAlert alertWithError:error];
925 [alert runModal];
926 }
927
928done:
929 self.temporaryMovieURL = nil;
930}
931
939- (void)stopRecording
940{
941 if (!_recorder)
942 return;
943
944 [_recorder finish];
945 [_recorder release];
946 _recorder = nil;
947
948 [self updateUI];
949
950 [self setFullScreen:NO onScreen:nil];
951
953 [self promptToSaveMovie];
955}
956
962- (void)cancelOperation:(id)sender
963{
964 [self setFullScreen:NO onScreen:nil];
965}
966
970- (void)close
971{
972 NSView *v = self.contentView;
973 VuoGraphicsLayer *gv = (VuoGraphicsLayer *)v.layer;
974 [gv close];
975
976 [super close];
977}
978
982- (void)dealloc
983{
984 [NSEvent removeMonitor:_mouseMonitor];
985
986 // https://b33p.net/kosada/node/12123
987 // Cocoa leaks the contentView unless we force it to resign firstResponder status.
988 [self makeFirstResponder:nil];
989
990 [super dealloc];
991}
992
993@end