diff --git a/CHANGES-macOS-modernization.md b/CHANGES-macOS-modernization.md new file mode 100644 index 0000000..6f8acff --- /dev/null +++ b/CHANGES-macOS-modernization.md @@ -0,0 +1,130 @@ +# SaveHollywood — macOS 26/27 modernization + +Changes made against `packagesdev/savehollywood` master (v2.6, last upstream activity 2018) +to make the screen saver build and run correctly on macOS 27 with Xcode 27. +Tested on macOS 27.0 beta (25A5388g), Xcode 27.0 beta (27A5228h), Apple Silicon. + +A machine-readable diff of every change is in `macos27-changes.patch` (`diff -ru` against +upstream master). + +--- + +## 1. Build configuration (`SaveHollywood.xcodeproj/project.pbxproj`) + +| Setting | Before | After | Why | +|---|---|---|---| +| `SDKROOT` (project level) | `macosx10.9` | `macosx` | The 10.9 SDK no longer exists; pinned SDK breaks the build immediately. | +| `MACOSX_DEPLOYMENT_TARGET` | `10.9` | `12.0` | Xcode 27 rejects deployment targets below 12.0. | +| `ARCHS` | `$(ARCHS_STANDARD_64_BIT)` | `$(ARCHS_STANDARD)` | Produces arm64. macOS 27 dropped Intel support; an x86_64-only `.saver` will not load. | +| Framework file references | absolute paths into `MacOSX10.14.sdk` | SDK-relative (`sourceTree = SDKROOT`) | The 10.14 SDK paths are stale; SDK-relative references track the active SDK. | +| `OTHER_LDFLAGS` | — | `-framework UniformTypeIdentifiers` | Needed for the `UTType`-based open-panel filter (see §3.1). | + +## 2. Deprecated API modernization (mechanical, no behavior change) + +* `NSOnState` / `NSOffState` → `NSControlStateValueOn` / `NSControlStateValueOff` +* `NSCompositeSourceOver` → `NSCompositingOperationSourceOver` +* `NSCenterTextAlignment` → `NSTextAlignmentCenter` +* `NSRegularControlSize` → `NSControlSizeRegular` +* `NSOKButton` / `NSFileHandlingPanelOKButton` → `NSModalResponseOK` +* `NSCalendarDate`/`yearOfCommonEra` → `NSCalendar component:fromDate:` (`SHAboutBoxWindowController.m`) +* `colorUsingColorSpaceName:` → `colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]` (`NSColor+String.m`) + +## 3. Functional fixes for modern macOS + +### 3.1 Folder selection broken in the open panel (`SHConfigurationWindowController.m`) + +`-[NSOpenPanel setAllowedFileTypes:]` with `[AVURLAsset audiovisualTypes]` now disables +directories in the panel even with `canChooseDirectories = YES` (older macOS ignored the +filter for folders). Replaced with `allowedContentTypes` built from `UTType` objects and +explicitly including `UTTypeFolder`. This is why the UniformTypeIdentifiers framework is +now linked. + +### 3.2 Drag & drop onto the assets table (`SHConfigurationWindowController.m`) + +`NSFilenamesPboardType` (deprecated 10.14) replaced with `NSPasteboardTypeFileURL`. +The two `propertyListForType:` reads were replaced with +`readObjectsForClasses:@[[NSURL class]]` + `NSPasteboardURLReadingFileURLsOnlyKey`, +mapped back to paths, so the downstream path-based logic is unchanged. + +### 3.3 Configure sheet would not dismiss (System Settings had to be force-quit) + +`[NSApp endSheet:]` no longer dismisses a configure sheet hosted by System Settings. +`closeDialog:` now uses `[self.window.sheetParent endSheet:returnCode:]` when a sheet +parent exists, falling back to the legacy call otherwise. + +### 3.4 Saver never stops after unlock (macOS 14+ `legacyScreenSaver` bug) + +On macOS 14 and later the system frequently never calls `stopAnimation` on legacy screen +savers after the user unlocks; `legacyScreenSaver` keeps running and audio keeps playing +indefinitely. Workaround (same approach the Aerial project uses): on `startAnimation` +(non-preview only) subscribe to the distributed notification `com.apple.screenIsUnlocked`; +on receipt, run `stopAnimation` (which also persists the resume position) and `exit(0)` +the host process. The system respawns `legacyScreenSaver` on demand. + +### 3.5 Preview instances play audio and stack up (`SaveHollywoodView.m`) + +The modern host can pass `isPreview == NO` for the small System Settings preview, and it +instantiates a new saver view per settings-pane visit without stopping the old one. Result: +multiple simultaneous audio streams. Two defenses: + +* Preview detection no longer trusts the flag alone: a frame ≤ 400×400 pt is treated as a + preview. Previews are always muted and never touch persisted state. +* `viewDidMoveToWindow` pauses the player whenever the view is detached from its window, + so leaked instances go silent even if the host never stops them. + +### 3.6 Resume-from-last-position broken (`SaveHollywoodView.m`) + +`screenIndex` required the saver window to be exactly contained in a screen frame +(`NSContainsRect`). The modern host's window does not match screen bounds exactly, so the +lookup returned `NSNotFound` and the resume position was silently neither saved nor +restored. The lookup now falls back to the screen with the largest intersection area, then +to screen 0. + +### 3.7 Insecure/deprecated archiving of resume data (`SaveHollywoodView.m`) + +* Removed the `Gestalt()` runtime version check and the `_useKeyedArchiverForLeftOffData` + flag: with a 12.0 floor the keyed-archiver path is always available, so the + `NSArchiver`/`NSUnarchiver` fallbacks (deprecated, insecure) were deleted along with the + legacy `screen#` defaults key. +* Writing: `archivedDataWithRootObject:requiringSecureCoding:YES error:` — failures are + now logged. +* Reading: `unarchivedObjectOfClasses:fromData:error:` with an allow-list of + `NSDictionary`, `NSString`, `NSValue`, `NSURL`. +* Old keyed data written by 2.6 remains readable; pre-10.12 `NSArchiver` data is dropped + (the resume position is transient, losing it once is harmless). + +### 3.8 Preview audio (`SaveHollywoodView.m`) + +Volume is now only applied when `_preview == NO`; preview playback is always muted. + +### 3.9 Main-display detection broken on multi-display setups (`SaveHollywoodView.m`) + +`_mainScreen` was decided at init time from the frame origin (`NSMinX/NSMinY == 0`), +which assumed saver windows use global screen coordinates. The modern host gives every +display's view a zero origin, so every instance considered itself the main display. This +broke both "Main display only" and "Play audio only on main display": every display played +video with its own audio stream, and the independent players drifted out of sync. +`_mainScreen` is now re-evaluated in `startAnimation` (when the window exists) by +comparing `self.window.screen` against `[NSScreen screens].firstObject` (the primary +display), with a largest-intersection `screenIndex` fallback. + +## 4. Known remaining deprecation warnings (functional, left as-is) + +* `-[AVAsset naturalSize]` (`SaveHollywoodView.m`) — the suggested replacement + (`tracksWithMediaType:`) is itself deprecated in favor of the async load API; migrating + means restructuring the (synchronous) layout path. +* `-[AVAsset loadValuesAsynchronouslyForKeys:completionHandler:]` — same situation. +* `-[NSWorkspace typeOfFile:error:]` (three sites) — replacement is + `NSURL getResourceValue:forKey:` with `NSURLContentTypeKey`; straightforward but was out + of scope for this pass. +* Project-file cruft reported by Xcode: duplicate xib entries in the Copy Bundle Resources + phase, a legacy Rez build phase, and `CFBundleIdentifier` vs. empty + `PRODUCT_BUNDLE_IDENTIFIER`. + +## 5. Verification performed + +* Release build succeeds with Xcode 27.0 beta / macOS 27 SDK, arm64, zero errors. +* Folder picking, drag & drop, sheet dismissal, playback, unlock shutdown, muted preview, + and resume-from-last-position exercised manually on macOS 27.0 beta. +* Secure-coding round-trip of the resume dictionary (`NSValue`+`CMTime`, `NSURL`) + verified with a standalone test program. diff --git a/SaveHollywood.xcodeproj/project.pbxproj b/SaveHollywood.xcodeproj/project.pbxproj index 34d5abb..30e34dc 100644 --- a/SaveHollywood.xcodeproj/project.pbxproj +++ b/SaveHollywood.xcodeproj/project.pbxproj @@ -67,11 +67,11 @@ F40B998A16AB1F6200644F61 /* NSColor+String.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSColor+String.m"; sourceTree = ""; }; F4121CCC16FD06200059A794 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localized.strings; sourceTree = ""; }; F434961A16B701B500C549DF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localized.strings; sourceTree = ""; }; - F44979562B53010100A1558A /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; - F44979582B53010B00A1558A /* ScreenSaver.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScreenSaver.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/ScreenSaver.framework; sourceTree = DEVELOPER_DIR; }; - F449795A2B53011B00A1558A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/AVFoundation.framework; sourceTree = DEVELOPER_DIR; }; - F449795C2B53017B00A1558A /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/CoreMedia.framework; sourceTree = DEVELOPER_DIR; }; - F449795E2B5301B200A1558A /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/QuartzCore.framework; sourceTree = DEVELOPER_DIR; }; + F44979562B53010100A1558A /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + F44979582B53010B00A1558A /* ScreenSaver.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScreenSaver.framework; path = System/Library/Frameworks/ScreenSaver.framework; sourceTree = SDKROOT; }; + F449795A2B53011B00A1558A /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; + F449795C2B53017B00A1558A /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + F449795E2B5301B200A1558A /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; F45A65162117329400E5A4D4 /* SHPlayingAssetsRegister.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SHPlayingAssetsRegister.h; sourceTree = ""; }; F45A65172117329400E5A4D4 /* SHPlayingAssetsRegister.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SHPlayingAssetsRegister.m; sourceTree = ""; }; F4875FC3211A50F1007D8759 /* SHView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SHView.h; sourceTree = ""; }; @@ -361,7 +361,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; @@ -379,9 +379,10 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 12.0; ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx10.9; + OTHER_LDFLAGS = "-framework UniformTypeIdentifiers"; + SDKROOT = macosx; }; name = Debug; }; @@ -389,7 +390,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; @@ -401,8 +402,9 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.9; - SDKROOT = macosx10.9; + MACOSX_DEPLOYMENT_TARGET = 12.0; + OTHER_LDFLAGS = "-framework UniformTypeIdentifiers"; + SDKROOT = macosx; }; name = Release; }; diff --git a/SaveHollywood/NSColor+String.m b/SaveHollywood/NSColor+String.m index 812776c..f5032ba 100644 --- a/SaveHollywood/NSColor+String.m +++ b/SaveHollywood/NSColor+String.m @@ -26,7 +26,7 @@ + (NSColor *)colorFromString:(NSString *)inString - (NSString *)stringValue { - NSColor *tColor = [self colorUsingColorSpaceName:@"NSCalibratedRGBColorSpace"]; + NSColor *tColor = [self colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; return([NSString stringWithFormat:@"%f|%f|%f",(float)[tColor redComponent],(float)[tColor greenComponent],(float)[tColor blueComponent]]); } diff --git a/SaveHollywood/SHAboutBoxWindowController.m b/SaveHollywood/SHAboutBoxWindowController.m index 64f7341..073e6fc 100644 --- a/SaveHollywood/SHAboutBoxWindowController.m +++ b/SaveHollywood/SHAboutBoxWindowController.m @@ -37,7 +37,7 @@ - (void)windowDidLoad _versionLabel.stringValue=[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Version %@ (%@)",@"Localized",tBundle,@""),tInfoDictionary[@"CFBundleShortVersionString"],tInfoDictionary[@"CFBundleVersion"]]; - _copyrightLabel.stringValue=[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Legal terms",@"Localized",tBundle,@""),[[NSCalendarDate date] yearOfCommonEra]]; + _copyrightLabel.stringValue=[NSString stringWithFormat:NSLocalizedStringFromTableInBundle(@"Legal terms",@"Localized",tBundle,@""),(long)[[NSCalendar currentCalendar] component:NSCalendarUnitYear fromDate:[NSDate date]]]; } @end diff --git a/SaveHollywood/SHConfigurationWindowController.m b/SaveHollywood/SHConfigurationWindowController.m index efc6519..520ae59 100644 --- a/SaveHollywood/SHConfigurationWindowController.m +++ b/SaveHollywood/SHConfigurationWindowController.m @@ -21,6 +21,7 @@ #import "SHUserDefaults+Constants.h" #import +#import #import "SHSlider.h" #import "SHAssetTableCellView.h" @@ -178,7 +179,7 @@ - (void)windowDidLoad // Register for D&D - [_assetsTableView registerForDraggedTypes:@[SHPasteboardTypeSelectedRows,NSFilenamesPboardType]]; + [_assetsTableView registerForDraggedTypes:@[SHPasteboardTypeSelectedRows,NSPasteboardTypeFileURL]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(shouldShowValueLabel:) @@ -212,13 +213,13 @@ - (void)refreshSettings BOOL tBool=[tDefaults boolForKey:SHUserDefaultsAssetsRandomOrder]; - [_randomOrderCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_randomOrderCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; // Start where left off tBool=[tDefaults boolForKey:SHUserDefaultsAssetsStartWhereLeftOff]; - [_resumePlayingCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_resumePlayingCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; // List @@ -257,19 +258,19 @@ - (void)refreshSettings tBool=[tDefaults boolForKey:SHUserDefaultsFrameRandomPosition]; - [_frameRandomPositionCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_frameRandomPositionCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; // Draw Border tBool=[tDefaults boolForKey:SHUserDefaultsFrameDrawBorder]; - [_frameDrawBorderCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_frameDrawBorderCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; // Show Metadata tBool=[tDefaults boolForKey:SHUserDefaultsFrameShowMetadata]; - [_frameShowMetatadaCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_frameShowMetatadaCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; [_frameShowMetatadaModeMatrix setEnabled:tBool]; @@ -306,7 +307,7 @@ - (void)refreshSettings tBool=[tDefaults boolForKey:SHUserDefaultsAudioMainDisplayOnly]; - [_audioMainScreenCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_audioMainScreenCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; // Volume @@ -339,7 +340,7 @@ - (void)refreshSettings tBool=[tDefaults boolForKey:SHUserDefaultsMainDisplayOnly]; - [_mainScreenCheckBox setState:(tBool==YES) ? NSOnState : NSOffState]; + [_mainScreenCheckBox setState:(tBool==YES) ? NSControlStateValueOn : NSControlStateValueOff]; [_assetsTableView reloadData]; } @@ -572,13 +573,23 @@ - (IBAction)addAsset:(id)sender [tOpenPanel setDelegate:self]; [tOpenPanel setCanChooseDirectories:YES]; [tOpenPanel setAllowsMultipleSelection:YES]; - [tOpenPanel setAllowedFileTypes:[AVURLAsset audiovisualTypes]]; + NSMutableArray * tContentTypes=[NSMutableArray arrayWithObject:UTTypeFolder]; + + for(NSString * tUTI in [AVURLAsset audiovisualTypes]) + { + UTType * tType=[UTType typeWithIdentifier:tUTI]; + + if (tType!=nil) + [tContentTypes addObject:tType]; + } + + [tOpenPanel setAllowedContentTypes:tContentTypes]; [tOpenPanel setTitle:NSLocalizedStringFromTableInBundle(@"Add video or folder",@"Localized",[NSBundle bundleForClass:[self class]],@"")]; [tOpenPanel setPrompt:NSLocalizedStringFromTableInBundle(@"Add",@"Localized",[NSBundle bundleForClass:[self class]],@"")]; NSInteger tResult=[tOpenPanel runModal]; - if (tResult==NSFileHandlingPanelOKButton) + if (tResult==NSModalResponseOK) { NSArray * tURLs=[tOpenPanel URLs]; NSMutableIndexSet * tMutableIndexSet=[NSMutableIndexSet indexSet]; @@ -648,7 +659,7 @@ - (IBAction)switchFrameScaling:(id)sender - (IBAction)switchShowMetadata:(id)sender { - BOOL tBool=([sender state]==NSOnState); + BOOL tBool=([sender state]==NSControlStateValueOn); NSInteger tTag=[[_frameShowMetatadaModeMatrix selectedCell] tag]; [_frameShowMetatadaModeMatrix setEnabled:tBool]; @@ -702,7 +713,7 @@ - (IBAction)showAboutBox:(id)sender - (IBAction)closeDialog:(id)sender { - if ([sender tag]==NSOKButton) + if ([sender tag]==NSModalResponseOK) { #ifdef __TEST_SCREENSAVER__ NSUserDefaults *tDefaults = [NSUserDefaults standardUserDefaults]; @@ -716,11 +727,11 @@ - (IBAction)closeDialog:(id)sender // Random Order - [tDefaults setBool:([_randomOrderCheckBox state]==NSOnState) forKey:SHUserDefaultsAssetsRandomOrder]; + [tDefaults setBool:([_randomOrderCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsAssetsRandomOrder]; // Start where left off - [tDefaults setBool:([_resumePlayingCheckBox state]==NSOnState) forKey:SHUserDefaultsAssetsStartWhereLeftOff]; + [tDefaults setBool:([_resumePlayingCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsAssetsStartWhereLeftOff]; // List @@ -747,15 +758,15 @@ - (IBAction)closeDialog:(id)sender // Random Position - [tDefaults setBool:([_frameRandomPositionCheckBox state]==NSOnState) forKey:SHUserDefaultsFrameRandomPosition]; + [tDefaults setBool:([_frameRandomPositionCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsFrameRandomPosition]; // Draw Border - [tDefaults setBool:([_frameDrawBorderCheckBox state]==NSOnState) forKey:SHUserDefaultsFrameDrawBorder]; + [tDefaults setBool:([_frameDrawBorderCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsFrameDrawBorder]; // Show Metadata - [tDefaults setBool:([_frameShowMetatadaCheckBox state]==NSOnState) forKey:SHUserDefaultsFrameShowMetadata]; + [tDefaults setBool:([_frameShowMetatadaCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsFrameShowMetadata]; // Show Metadata mode @@ -780,7 +791,7 @@ - (IBAction)closeDialog:(id)sender // Audio - [tDefaults setBool:([_audioMainScreenCheckBox state]==NSOnState) forKey:SHUserDefaultsAudioMainDisplayOnly]; + [tDefaults setBool:([_audioMainScreenCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsAudioMainDisplayOnly]; // Volume @@ -793,14 +804,17 @@ - (IBAction)closeDialog:(id)sender // Main Screen Only - [tDefaults setBool:([_mainScreenCheckBox state]==NSOnState) forKey:SHUserDefaultsMainDisplayOnly]; + [tDefaults setBool:([_mainScreenCheckBox state]==NSControlStateValueOn) forKey:SHUserDefaultsMainDisplayOnly]; [tDefaults synchronize]; } [_assetsTableView deselectAll:nil]; - [NSApp endSheet:self.window]; + if (self.window.sheetParent!=nil) + [self.window.sheetParent endSheet:self.window returnCode:NSModalResponseOK]; + else + [NSApp endSheet:self.window]; } #pragma mark - NSTableView DataSource @@ -939,7 +953,7 @@ - (NSDragOperation)tableView:(NSTableView *)inTableView validateDrop:(id )inD NSMutableArray * tNewAssets=nil; NSPasteboard * tPasteboard=[inDraggingInfo draggingPasteboard]; - NSString * tPasteboardType=[tPasteboard availableTypeFromArray:@[SHPasteboardTypeSelectedRows,NSFilenamesPboardType]]; + NSString * tPasteboardType=[tPasteboard availableTypeFromArray:@[SHPasteboardTypeSelectedRows,NSPasteboardTypeFileURL]]; if ([tPasteboardType isEqualToString:SHPasteboardTypeSelectedRows]==YES) { @@ -1047,9 +1061,9 @@ - (BOOL)tableView:(NSTableView *)inTableView acceptDrop:(id )inD [_cachedAssetsArray removeObjectsAtIndexes:_internalDragData]; } - else if ([tPasteboardType isEqualToString:NSFilenamesPboardType]==YES) + else if ([tPasteboardType isEqualToString:NSPasteboardTypeFileURL]==YES) { - NSArray * tFilesArray = [tPasteboard propertyListForType:NSFilenamesPboardType]; + NSArray * tFilesArray = [[tPasteboard readObjectsForClasses:@[[NSURL class]] options:@{NSPasteboardURLReadingFileURLsOnlyKey:@(YES)}] valueForKey:@"path"]; NSFileManager * tFileManager=[NSFileManager defaultManager]; tNewAssets=[NSMutableArray array]; diff --git a/SaveHollywood/SHLightGrayBackgroundView.m b/SaveHollywood/SHLightGrayBackgroundView.m index e994c8b..2cc822c 100644 --- a/SaveHollywood/SHLightGrayBackgroundView.m +++ b/SaveHollywood/SHLightGrayBackgroundView.m @@ -29,7 +29,7 @@ - (void)drawRect:(NSRect)dirtyRect [[NSColor colorWithDeviceWhite:0.915 alpha:0.8] set]; - NSRectFillUsingOperation(dirtyRect, NSCompositeSourceOver); + NSRectFillUsingOperation(dirtyRect, NSCompositingOperationSourceOver); } @end diff --git a/SaveHollywood/SHSecondaryBox.m b/SaveHollywood/SHSecondaryBox.m index a3794af..7d46760 100644 --- a/SaveHollywood/SHSecondaryBox.m +++ b/SaveHollywood/SHSecondaryBox.m @@ -29,13 +29,13 @@ - (void)drawRect:(NSRect)dirtyRect else [[NSColor colorWithDeviceWhite:0.0 alpha:0.15] set]; - NSRectFillUsingOperation(dirtyRect,NSCompositeSourceOver); + NSRectFillUsingOperation(dirtyRect,NSCompositingOperationSourceOver); [[NSColor colorWithDeviceWhite:0.0 alpha:0.10] set]; NSRect tFrameRect=NSInsetRect([self bounds],-1,0); - NSFrameRectWithWidthUsingOperation(tFrameRect, 1.0, NSCompositeSourceOver); + NSFrameRectWithWidthUsingOperation(tFrameRect, 1.0, NSCompositingOperationSourceOver); } @end diff --git a/SaveHollywood/SHTokenView.m b/SaveHollywood/SHTokenView.m index 9ec0d79..b19782e 100644 --- a/SaveHollywood/SHTokenView.m +++ b/SaveHollywood/SHTokenView.m @@ -43,14 +43,14 @@ - (id)initWithFrame:(NSRect)frameRect if (self!=nil) { NSMutableParagraphStyle * tMutableParagraphStyle=[[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - tMutableParagraphStyle.alignment=NSCenterTextAlignment; + tMutableParagraphStyle.alignment=NSTextAlignmentCenter; NSShadow * tShadow=[NSShadow new]; tShadow.shadowOffset=NSMakeSize(0,-1); tShadow.shadowColor=[NSColor colorWithDeviceWhite:0.65 alpha:1.0]; _attributesDictionary=[@{NSForegroundColorAttributeName:[NSColor whiteColor], - NSFontAttributeName:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]], + NSFontAttributeName:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSControlSizeRegular]], NSParagraphStyleAttributeName:tMutableParagraphStyle, NSShadowAttributeName:tShadow} copy]; @@ -101,7 +101,7 @@ - (void)drawRect:(NSRect)dirtyRect if (_cachedStringValue!=nil) { - CGFloat tHeight=[NSFont systemFontSizeForControlSize:NSRegularControlSize];//heightForStringDrawing(_cachedStringValue,tBounds.size.width); + CGFloat tHeight=[NSFont systemFontSizeForControlSize:NSControlSizeRegular];//heightForStringDrawing(_cachedStringValue,tBounds.size.width); tBounds.origin.y=round(NSMidY(tBounds)-tHeight*0.5+1); tBounds.size.height=tHeight; diff --git a/SaveHollywood/SaveHollywoodView.m b/SaveHollywood/SaveHollywoodView.m index c9c7261..2cf310b 100755 --- a/SaveHollywood/SaveHollywoodView.m +++ b/SaveHollywood/SaveHollywoodView.m @@ -30,7 +30,6 @@ #define METADATA_DISPLAY_DURATION 5.0 -NSString * const SHScreenKey=@"screen#"; NSString * const SHScreenKeyKeyed=@"screen.keyed#"; NSString * const SHAssetTimeKey=@"asset.time"; NSString * const SHAssetURLKey=@"asset.url"; @@ -70,10 +69,6 @@ @interface SaveHollywoodView () SHMovieAudioVolumeMode _volumeMode; float _volumeLevel; - // Workaround for Apple bug in Sierra - - BOOL _useKeyedArchiverForLeftOffData; - // Layers CALayer * _backgroundLayer; @@ -151,21 +146,15 @@ - (id)initWithFrame:(NSRect)frameRect isPreview:(BOOL)isPreview if (self!=nil) { - SInt32 tMajorVersion,tMinorVersion,tBugFixVersion; - - Gestalt(gestaltSystemVersionMajor,&tMajorVersion); - Gestalt(gestaltSystemVersionMinor,&tMinorVersion); - Gestalt(gestaltSystemVersionBugFix,&tBugFixVersion); - - - _useKeyedArchiverForLeftOffData=(tMajorVersion>10 || (tMajorVersion==10 && tMinorVersion>=12)); - [self setAnimationTimeInterval:1.0]; _fileManager=[NSFileManager defaultManager]; - _preview=isPreview; - + // The modern legacyScreenSaver host can pass isPreview=NO for the System Settings preview; + // a small frame is a preview regardless of the flag + + _preview=(isPreview==YES || (frameRect.size.width<=400.0 && frameRect.size.height<=400.0)); + if (_preview==YES) { _mainScreen=YES; @@ -185,6 +174,16 @@ - (id)initWithFrame:(NSRect)frameRect isPreview:(BOOL)isPreview #pragma mark - +- (void)viewDidMoveToWindow +{ + [super viewDidMoveToWindow]; + + // The modern legacyScreenSaver host does not always stop animation before discarding a view + + if (self.window==nil) + [_AVPlayerLayer.player pause]; +} + - (void)keyDown:(NSEvent *) inEvent { if (_preview==NO) @@ -259,17 +258,34 @@ - (NSUInteger)screenIndex NSArray * tScreensArray=[NSScreen screens]; __block NSUInteger tFoundIndex=NSNotFound; + __block CGFloat tLargestArea=0.0; + [tScreensArray enumerateObjectsUsingBlock:^(NSScreen * bScreen, NSUInteger bIndex, BOOL *bOutStop) { - + NSRect tScreenFrame=[bScreen frame]; - + if (NSContainsRect(tScreenFrame,tWindowFrame)==YES) { tFoundIndex=bIndex; *bOutStop=YES; + return; + } + + // The window of the modern legacyScreenSaver host does not always match the screen frame exactly + + NSRect tIntersectionRect=NSIntersectionRect(tScreenFrame,tWindowFrame); + CGFloat tArea=tIntersectionRect.size.width*tIntersectionRect.size.height; + + if (tArea>tLargestArea) + { + tLargestArea=tArea; + tFoundIndex=bIndex; } }]; - + + if (tFoundIndex==NSNotFound && [tScreensArray count]>0) + tFoundIndex=0; + return tFoundIndex; } @@ -284,10 +300,25 @@ - (void)startAnimation ScreenSaverDefaults *tDefaults = [ScreenSaverDefaults defaultsForModuleWithName:tIdentifier]; SHSettings * tSettings=[SHSettings settings]; - + [super startAnimation]; + + // Workaround for macOS 14+ bug where legacyScreenSaver is never told to stop after unlocking + + if (_preview==NO) + { + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(screenIsUnlocked:) name:@"com.apple.screenIsUnlocked" object:nil]; + + // The frame origin heuristic used at init time fails in the modern legacyScreenSaver host + // where every view has a zero origin; determine the actual screen now that the window exists + + if (self.window.screen!=nil) + _mainScreen=(self.window.screen==[NSScreen screens].firstObject); + else if (self.window!=nil) + _mainScreen=([self screenIndex]==0); + } #endif - + BOOL tBool=tSettings.mainDisplayOnly; if (tBool==NO || _mainScreen==YES) @@ -489,12 +520,7 @@ - (void)startAnimation if (tScreenIndex!=NSNotFound) { - NSString * tScreenKey; - - if (_useKeyedArchiverForLeftOffData==YES) - tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKeyKeyed,(unsigned long)tScreenIndex]; - else - tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKey,(unsigned long)tScreenIndex]; + NSString * tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKeyKeyed,(unsigned long)tScreenIndex]; if (tSettings.startWhereLeftOff==YES) { @@ -502,10 +528,7 @@ - (void)startAnimation if (tData!=nil) { - if (_useKeyedArchiverForLeftOffData==YES) - tLastKnownAssetDictionary=[NSKeyedUnarchiver unarchiveObjectWithData:tData]; - else - tLastKnownAssetDictionary=[NSUnarchiver unarchiveObjectWithData:tData]; + tLastKnownAssetDictionary=[NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSDictionary class],[NSString class],[NSValue class],[NSURL class],nil] fromData:tData error:NULL]; if (tLastKnownAssetDictionary==nil) NSLog(@"Error when unarchiving last known asset for %@",tScreenKey); @@ -598,12 +621,7 @@ - (void)stopAnimation if (tScreenIndex!=NSNotFound) { - NSString * tScreenKey; - - if (_useKeyedArchiverForLeftOffData==YES) - tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKeyKeyed,(unsigned long)tScreenIndex]; - else - tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKey,(unsigned long)tScreenIndex]; + NSString * tScreenKey=[NSString stringWithFormat:@"%@%lu",SHScreenKeyKeyed,(unsigned long)tScreenIndex]; if (tSettings.startWhereLeftOff==YES) { @@ -617,15 +635,13 @@ - (void)stopAnimation NSDictionary * tLastAssetDictionary=@{SHAssetTimeKey:tValue, SHAssetURLKey:tCurrentURL}; - NSData * tData=nil; - - if (_useKeyedArchiverForLeftOffData==YES) - tData=[NSKeyedArchiver archivedDataWithRootObject:tLastAssetDictionary]; - else - tData=[NSArchiver archivedDataWithRootObject:tLastAssetDictionary]; - + NSError * tError=nil; + NSData * tData=[NSKeyedArchiver archivedDataWithRootObject:tLastAssetDictionary requiringSecureCoding:YES error:&tError]; + if (tData!=nil) [tDefaults setObject:tData forKey:tScreenKey]; + else + NSLog(@"SaveHollywood: error when archiving last known asset: %@",tError); } } else @@ -670,10 +686,25 @@ - (void)stopAnimation _volumeLevelHasBeenModified=NO; #ifndef __TEST_SCREENSAVER__ + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self name:@"com.apple.screenIsUnlocked" object:nil]; + [super stopAnimation]; #endif } +- (void)screenIsUnlocked:(NSNotification *)inNotification +{ + // Workaround for macOS 14+ bug where legacyScreenSaver keeps running after unlocking + + if (_preview==YES) + return; + + if ([self isAnimating]==YES) + [self stopAnimation]; + + exit(0); +} + - (BOOL)playNextAsset:(NSDictionary *)preferredNextAssetDictionary canPlaySameRandomMovieTwice:(BOOL)inCanPlaySameRandomMovieTwice { NSUInteger tCount=[__assetsArray count]; @@ -914,7 +945,7 @@ - (BOOL)playNextAsset:(NSDictionary *)preferredNextAssetDictionary canPlaySameRa default: - if (_audioMainScreen==NO || _mainScreen==YES) + if (_preview==NO && (_audioMainScreen==NO || _mainScreen==YES)) { _AVPlayerLayer.player.volume=(_liveMuted==YES) ? 0.0f :_volumeLevel; }