From c55baa31748ca67097c017ead797b361cc58b9e6 Mon Sep 17 00:00:00 2001 From: hectorlizard Date: Mon, 23 Mar 2026 15:33:23 +0100 Subject: [PATCH] feat: Support modern App Extensions for macOS 10.15+ --- ModernAppExtension/App/AppDelegate.swift | 10 +++ ModernAppExtension/App/Info.plist | 69 +++++++++++++++ .../PreviewExtension/Info.plist | 41 +++++++++ .../PreviewViewController.swift | 36 ++++++++ ModernAppExtension/README.md | 29 +++++++ .../ThumbnailExtension/Info.plist | 41 +++++++++ .../ThumbnailProvider.swift | 37 +++++++++ ModernAppExtension/project.yml | 83 +++++++++++++++++++ README.md | 17 +++- 9 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 ModernAppExtension/App/AppDelegate.swift create mode 100644 ModernAppExtension/App/Info.plist create mode 100644 ModernAppExtension/PreviewExtension/Info.plist create mode 100644 ModernAppExtension/PreviewExtension/PreviewViewController.swift create mode 100644 ModernAppExtension/README.md create mode 100644 ModernAppExtension/ThumbnailExtension/Info.plist create mode 100644 ModernAppExtension/ThumbnailExtension/ThumbnailProvider.swift create mode 100644 ModernAppExtension/project.yml diff --git a/ModernAppExtension/App/AppDelegate.swift b/ModernAppExtension/App/AppDelegate.swift new file mode 100644 index 0000000..5dd36c6 --- /dev/null +++ b/ModernAppExtension/App/AppDelegate.swift @@ -0,0 +1,10 @@ +import Cocoa + +@main +class AppDelegate: NSObject, NSApplicationDelegate { + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Just print and exit gracefully, we just need Finder to register UTIs and Extensions. + print("GLTFQuickLook App registered. You can close this app.") + NSApplication.shared.terminate(nil) + } +} diff --git a/ModernAppExtension/App/Info.plist b/ModernAppExtension/App/Info.plist new file mode 100644 index 0000000..6c192aa --- /dev/null +++ b/ModernAppExtension/App/Info.plist @@ -0,0 +1,69 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSUIElement + + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + + UTTypeDescription + glTF Document + UTTypeIdentifier + org.khronos.gltf + UTTypeTagSpecification + + public.filename-extension + + gltf + + public.mime-type + + model/gltf+json + + + + + UTTypeConformsTo + + public.data + + UTTypeDescription + glTF Binary Document + UTTypeIdentifier + org.khronos.glb + UTTypeTagSpecification + + public.filename-extension + + glb + + public.mime-type + + model/gltf-binary + + + + + + diff --git a/ModernAppExtension/PreviewExtension/Info.plist b/ModernAppExtension/PreviewExtension/Info.plist new file mode 100644 index 0000000..351d880 --- /dev/null +++ b/ModernAppExtension/PreviewExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + GLTF Preview + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + org.khronos.gltf + org.khronos.glb + + QLSupportsSearchableItems + + + NSExtensionPointIdentifier + com.apple.quicklook.preview + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PreviewViewController + + + diff --git a/ModernAppExtension/PreviewExtension/PreviewViewController.swift b/ModernAppExtension/PreviewExtension/PreviewViewController.swift new file mode 100644 index 0000000..fd2ab4e --- /dev/null +++ b/ModernAppExtension/PreviewExtension/PreviewViewController.swift @@ -0,0 +1,36 @@ +import Cocoa +import Quartz +import SceneKit +import GLTFSceneKit + +class PreviewViewController: NSViewController, QLPreviewingController { + + var sceneView: SCNView! + + override func loadView() { + self.sceneView = SCNView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) + self.sceneView.autoresizingMask = [.width, .height] + self.sceneView.backgroundColor = NSColor.windowBackgroundColor + self.sceneView.allowsCameraControl = true + self.sceneView.autoenablesDefaultLighting = true + self.view = self.sceneView + } + + func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) { + DispatchQueue.global(qos: .userInitiated).async { + do { + let source = try GLTFSceneSource(url: url) + let scene = try source.scene() + + DispatchQueue.main.async { + self.sceneView.scene = scene + handler(nil) + } + } catch { + DispatchQueue.main.async { + handler(error) + } + } + } + } +} diff --git a/ModernAppExtension/README.md b/ModernAppExtension/README.md new file mode 100644 index 0000000..3a71aad --- /dev/null +++ b/ModernAppExtension/README.md @@ -0,0 +1,29 @@ +# GLTFQuickLook Modern App Extension + +This folder contains the modern macOS App Extension implementation of GLTFQuickLook for macOS 10.15 (Catalina) and newer, including full support for Apple Silicon (M1/M2/M3) and upcoming macOS versions like Sequoia. + +Apple deprecated `.qlgenerator` plugins and requires QuickLook extensions to be embedded inside a macOS application bag (`.app`). + +## Installation + +1. Download the latest `GLTFQuickLook.app` release. +2. Drag and drop `GLTFQuickLook.app` into your `/Applications` folder. +3. Open the app once (it will launch and exit immediately, registering the QuickLook extensions with macOS). +4. Select any `.gltf` or `.glb` file in Finder and press `Space` to preview. + +## Build Setup + +To build this modern extension from source, you need [XcodeGen](https://github.com/yonaskolb/XcodeGen) and macOS 12.0+ with Xcode. + +```bash +# Install XcodeGen +brew install xcodegen + +# Generate the Xcode Project +xcodegen + +# Open the project +open GLTFQuickLook.xcodeproj +``` + +You can then build the `GLTFQuickLook` scheme directly from Xcode. Swift Package Manager will automatically fetch `GLTFSceneKit`. diff --git a/ModernAppExtension/ThumbnailExtension/Info.plist b/ModernAppExtension/ThumbnailExtension/Info.plist new file mode 100644 index 0000000..e304708 --- /dev/null +++ b/ModernAppExtension/ThumbnailExtension/Info.plist @@ -0,0 +1,41 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + GLTF Thumbnail + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + XPC! + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + org.khronos.gltf + org.khronos.glb + + QLThumbnailMinimumDimension + 0 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ThumbnailProvider + + + diff --git a/ModernAppExtension/ThumbnailExtension/ThumbnailProvider.swift b/ModernAppExtension/ThumbnailExtension/ThumbnailProvider.swift new file mode 100644 index 0000000..f1c9780 --- /dev/null +++ b/ModernAppExtension/ThumbnailExtension/ThumbnailProvider.swift @@ -0,0 +1,37 @@ +import Cocoa +import QuickLookThumbnailing +import SceneKit +import GLTFSceneKit + +class ThumbnailProvider: QLThumbnailProvider { + + override func provideThumbnail(for request: QLFileThumbnailRequest, _ handler: @escaping (QLThumbnailReply?, Error?) -> Void) { + let size = request.maximumSize + + // Use a QLThumbnailReply with drawing context to correctly handle scaling + let reply = QLThumbnailReply(contextSize: size) { () -> Bool in + guard let context = NSGraphicsContext.current?.cgContext else { return false } + + do { + let source = try GLTFSceneSource(url: request.fileURL) + let scene = try source.scene() + + let renderer = SCNRenderer(device: nil, options: nil) + renderer.scene = scene + renderer.autoenablesDefaultLighting = true + + let image = renderer.snapshot(atTime: 0.0, with: size, antialiasingMode: .multisampling4X) + + if let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) { + context.draw(cgImage, in: CGRect(origin: .zero, size: size)) + return true + } + } catch { + print("Error generating thumbnail: \(error)") + } + return false + } + + handler(reply, nil) + } +} diff --git a/ModernAppExtension/project.yml b/ModernAppExtension/project.yml new file mode 100644 index 0000000..a4ca89d --- /dev/null +++ b/ModernAppExtension/project.yml @@ -0,0 +1,83 @@ +name: GLTFQuickLook +options: + bundleIdPrefix: com.hectorlizard +settings: + CODE_SIGN_IDENTITY: "-" + MARKETING_VERSION: "1.0" + CURRENT_PROJECT_VERSION: "1" +packages: + GLTFSceneKit: + url: https://github.com/magicien/GLTFSceneKit.git + branch: master +targets: + GLTFQuickLook: + type: application + platform: macOS + deploymentTarget: "12.0" + sources: App + settings: + PRODUCT_BUNDLE_IDENTIFIER: com.hectorlizard.GLTFQuickLook + info: + path: App/Info.plist + properties: + LSUIElement: true + UTExportedTypeDeclarations: + - UTTypeIdentifier: org.khronos.gltf + UTTypeConformsTo: [public.data] + UTTypeDescription: glTF Document + UTTypeTagSpecification: + public.filename-extension: [gltf] + public.mime-type: [model/gltf+json] + - UTTypeIdentifier: org.khronos.glb + UTTypeConformsTo: [public.data] + UTTypeDescription: glTF Binary Document + UTTypeTagSpecification: + public.filename-extension: [glb] + public.mime-type: [model/gltf-binary] + dependencies: + - target: GLTFPreview + - target: GLTFThumbnail + + GLTFPreview: + type: app-extension + platform: macOS + deploymentTarget: "12.0" + sources: PreviewExtension + settings: + PRODUCT_BUNDLE_IDENTIFIER: com.hectorlizard.GLTFQuickLook.GLTFPreview + ENABLE_HARDENED_RUNTIME: YES + ENABLE_APP_SANDBOX: YES + info: + path: PreviewExtension/Info.plist + properties: + CFBundleDisplayName: GLTF Preview + NSExtension: + NSExtensionAttributes: + QLSupportedContentTypes: [org.khronos.gltf, org.khronos.glb] + QLSupportsSearchableItems: true + NSExtensionPointIdentifier: com.apple.quicklook.preview + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).PreviewViewController + dependencies: + - package: GLTFSceneKit + + GLTFThumbnail: + type: app-extension + platform: macOS + deploymentTarget: "12.0" + sources: ThumbnailExtension + settings: + PRODUCT_BUNDLE_IDENTIFIER: com.hectorlizard.GLTFQuickLook.GLTFThumbnail + ENABLE_HARDENED_RUNTIME: YES + ENABLE_APP_SANDBOX: YES + info: + path: ThumbnailExtension/Info.plist + properties: + CFBundleDisplayName: GLTF Thumbnail + NSExtension: + NSExtensionAttributes: + QLSupportedContentTypes: [org.khronos.gltf, org.khronos.glb] + QLThumbnailMinimumDimension: 0 + NSExtensionPointIdentifier: com.apple.quicklook.thumbnail + NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ThumbnailProvider + dependencies: + - package: GLTFSceneKit diff --git a/README.md b/README.md index 0b0f5d2..bc1d05b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,18 @@ macOS QuickLook plugin for glTF files. (.gltf/.glb) - macOS 10.13 (High Sierra) or later -## Install +> **Note for macOS 10.15+ and Apple Silicon**: Apple has deprecated `.qlgenerator` plugins and they are fully unsupported in macOS 15 (Sequoia). There are now two versions of this project available: +> - **Legacy** (`.qlgenerator`): For macOS 10.13 and 10.14 (see instructions below). +> - **Modern** (App Extension): For macOS 10.15+, including native Apple Silicon support. See the [ModernAppExtension folder](ModernAppExtension/) for details. + +## Install (Modern - macOS 10.15+) + +1. Download the latest `GLTFQuickLook.app` release from [Releases](https://github.com/magicien/GLTFQuickLook/releases/latest). +2. Drag and drop `GLTFQuickLook.app` into your `/Applications` folder. +3. Open the app **once** (it will casually launch and exit, registering the extension with macOS). +4. Run `qlmanage -r` to reload QuickLook plugins. + +## Install (Legacy - macOS 10.13, 10.14) ### Using [Homebrew Cask](https://github.com/phinze/homebrew-cask) @@ -25,6 +36,10 @@ macOS QuickLook plugin for glTF files. (.gltf/.glb) ## Build +### Modern App Extension (Recommended for macOS 12+) +See the [ModernAppExtension/README.md](ModernAppExtension/README.md) for build instructions using `xcodegen` and Swift Package Manager. + +### Legacy (.qlgenerator) It needs to install [Carthage](https://github.com/Carthage/Carthage) to get frameworks. ``` $ git clone https://github.com/magicien/GLTFQuickLook.git