Skip to content

Commit b77d623

Browse files
benwafflecbjeukendrup
authored andcommitted
Add quicklook and thumbnail plugins
1 parent a11a200 commit b77d623

File tree

10 files changed

+457
-2
lines changed

10 files changed

+457
-2
lines changed

src/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ if (MUE_CONFIGURATION_IS_APPWEB)
8686
add_subdirectory(webbridge)
8787
endif()
8888

89+
add_subdirectory(macos-integration)
90+
8991
# Stubs
9092
add_subdirectory(stubs)
91-
92-

src/macos-integration/.gitignore

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Xcode
2+
3+
## User settings
4+
xcuserdata/
5+
6+
## Obj-C/Swift specific
7+
*.hmap
8+
9+
## App packaging
10+
*.ipa
11+
*.dSYM.zip
12+
*.dSYM
13+
14+
## Playgrounds
15+
timeline.xctimeline
16+
playground.xcworkspace
17+
18+
# Swift Package Manager
19+
#
20+
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
21+
Packages/
22+
Package.pins
23+
Package.resolved
24+
*.xcodeproj
25+
#
26+
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
27+
# hence it is not needed unless you have added a package configuration file to your project
28+
.swiftpm
29+
30+
.build/
31+
32+
# CocoaPods
33+
#
34+
# We recommend against adding the Pods directory to your .gitignore. However
35+
# you should judge for yourself, the pros and cons are mentioned at:
36+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
37+
#
38+
# Pods/
39+
#
40+
# Add this line if you want to avoid checking in source code from the Xcode workspace
41+
# *.xcworkspace
42+
43+
# Carthage
44+
#
45+
# Add this line if you want to avoid checking in source code from Carthage dependencies.
46+
# Carthage/Checkouts
47+
48+
Carthage/Build/
49+
50+
# fastlane
51+
#
52+
# It is recommended to not store the screenshots in the git repo.
53+
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
54+
# For more information about the recommended setup visit:
55+
# https://docs.fastlane.tools/best-practices/source-control/#source-control
56+
57+
fastlane/report.xml
58+
fastlane/Preview.html
59+
fastlane/screenshots/**/*.png
60+
fastlane/test_output
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# SPDX-License-Identifier: GPL-3.0-only
2+
# MuseScore-Studio-CLA-applies
3+
#
4+
# MuseScore Studio
5+
# Music Composition & Notation
6+
#
7+
# Copyright (C) 2024 MuseScore Limited
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License version 3 as
11+
# published by the Free Software Foundation.
12+
#
13+
# This program is distributed in the hope that it will be useful,
14+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
# GNU General Public License for more details.
17+
#
18+
# You should have received a copy of the GNU General Public License
19+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
21+
if(NOT OS_IS_MAC)
22+
return()
23+
endif()
24+
25+
set(SWIFT_BUILD_DIR "${CMAKE_BINARY_DIR}/swift_build")
26+
file(MAKE_DIRECTORY ${SWIFT_BUILD_DIR})
27+
28+
file(GLOB_RECURSE SWIFT_SOURCES
29+
"${CMAKE_CURRENT_SOURCE_DIR}/**/*.swift"
30+
"${CMAKE_CURRENT_SOURCE_DIR}/**/*.plist"
31+
"${CMAKE_CURRENT_SOURCE_DIR}/**/*.entitlements"
32+
"${CMAKE_CURRENT_SOURCE_DIR}/Package.swift"
33+
)
34+
35+
set(SWIFT_BUILD_TYPE $<IF:$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>,debug,release>)
36+
37+
add_custom_command(
38+
OUTPUT "${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreQLPreviewProvider"
39+
"${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreThumbnailProvider"
40+
COMMAND swift package resolve
41+
COMMAND swift build --build-path ${SWIFT_BUILD_DIR} -c ${SWIFT_BUILD_TYPE}
42+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
43+
DEPENDS ${SWIFT_SOURCES}
44+
COMMENT "Building swift code"
45+
)
46+
47+
add_custom_target(swift_package_build ALL
48+
DEPENDS "${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreQLPreviewProvider"
49+
"${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreThumbnailProvider"
50+
)
51+
52+
# Install targets
53+
install(CODE "
54+
# Create directory structure
55+
file(MAKE_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreQLPreviewProvider.appex/Contents/MacOS\")
56+
file(MAKE_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreThumbnailProvider.appex/Contents/MacOS\")
57+
58+
# Copy Info.plist files
59+
file(COPY \"${CMAKE_CURRENT_SOURCE_DIR}/Sources/MuseScoreQLPreviewProvider/Info.plist\"
60+
DESTINATION \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreQLPreviewProvider.appex/Contents\")
61+
file(COPY \"${CMAKE_CURRENT_SOURCE_DIR}/Sources/MuseScoreThumbnailProvider/Info.plist\"
62+
DESTINATION \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreThumbnailProvider.appex/Contents\")
63+
64+
# Copy executable to MacOS
65+
file(COPY \"${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreQLPreviewProvider\"
66+
DESTINATION \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreQLPreviewProvider.appex/Contents/MacOS\")
67+
file(COPY \"${SWIFT_BUILD_DIR}/${SWIFT_BUILD_TYPE}/MuseScoreThumbnailProvider\"
68+
DESTINATION \"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreThumbnailProvider.appex/Contents/MacOS\")
69+
70+
# Ad-hoc code sign the QuickLook extension, for dev
71+
execute_process(
72+
COMMAND codesign --force --sign - --timestamp=none --entitlements \"${CMAKE_CURRENT_SOURCE_DIR}/plugin.entitlements\"
73+
\"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreQLPreviewProvider.appex\"
74+
RESULT_VARIABLE CODESIGN_RESULT
75+
)
76+
if(NOT CODESIGN_RESULT EQUAL 0)
77+
message(FATAL_ERROR \"Failed to ad-hoc sign QuickLook extension\")
78+
endif()
79+
80+
# Ad-hoc code sign the Thumbnail Provider extension, for dev
81+
execute_process(
82+
COMMAND codesign --force --sign - --timestamp=none --entitlements \"${CMAKE_CURRENT_SOURCE_DIR}/plugin.entitlements\"
83+
\"${CMAKE_INSTALL_PREFIX}/mscore.app/Contents/PlugIns/MuseScoreThumbnailProvider.appex\"
84+
RESULT_VARIABLE CODESIGN_RESULT
85+
)
86+
if(NOT CODESIGN_RESULT EQUAL 0)
87+
message(FATAL_ERROR \"Failed to ad-hoc sign Thumbnail Provider extension\")
88+
endif()
89+
90+
# Ad-hoc code sign the main app, for dev
91+
execute_process(
92+
COMMAND codesign --force --sign - --timestamp=none
93+
\"${CMAKE_INSTALL_PREFIX}/mscore.app\"
94+
RESULT_VARIABLE CODESIGN_RESULT
95+
)
96+
if(NOT CODESIGN_RESULT EQUAL 0)
97+
message(FATAL_ERROR \"Failed to ad-hoc sign main app\")
98+
endif()
99+
")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// swift-tools-version:5.4
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "MuseScoreQuickLook",
6+
platforms: [
7+
.macOS("12.0")
8+
],
9+
products: [
10+
.executable(
11+
name: "MuseScoreQLPreviewProvider",
12+
targets: ["MuseScoreQLPreviewProvider"]
13+
),
14+
.executable(
15+
name: "MuseScoreThumbnailProvider",
16+
targets: ["MuseScoreThumbnailProvider"]
17+
)
18+
],
19+
dependencies: [
20+
.package(url: "https://github.com/weichsel/ZIPFoundation.git", .upToNextMajor(from: "0.9.0"))
21+
],
22+
targets: [
23+
.executableTarget(
24+
name: "MuseScoreQLPreviewProvider",
25+
dependencies: ["ZIPFoundation"],
26+
exclude: ["Info.plist"],
27+
swiftSettings: [
28+
.unsafeFlags(["-application-extension"])
29+
],
30+
linkerSettings: [
31+
.unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"])
32+
]
33+
),
34+
.executableTarget(
35+
name: "MuseScoreThumbnailProvider",
36+
dependencies: ["ZIPFoundation"],
37+
exclude: ["Info.plist"],
38+
swiftSettings: [
39+
.unsafeFlags(["-application-extension"])
40+
],
41+
linkerSettings: [
42+
.unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"])
43+
]
44+
)
45+
]
46+
)

src/macos-integration/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# MuseScore MacOS QuickLook Preview Provider & Thumbnail Provider
2+
3+
To check if the two plugins are loaded by macOS, run
4+
5+
```bash
6+
pluginkit -m -v | grep muse
7+
```
8+
9+
To view the logs for the plugin, run
10+
11+
```bash
12+
log stream --level info --predicate 'process == "MuseScoreQLPreviewProvider" OR process == "MuseScoreThumbnailProvider"'
13+
```
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleIdentifier</key>
6+
<string>org.musescore.MuseScoreQLPreviewProvider</string>
7+
<key>CFBundleName</key>
8+
<string>MuseScoreQLPreviewProvider</string>
9+
<key>CFBundleExecutable</key>
10+
<string>MuseScoreQLPreviewProvider</string>
11+
<key>CFBundlePackageType</key>
12+
<string>XPC!</string>
13+
<key>CFBundleShortVersionString</key>
14+
<string>1.0</string>
15+
<key>CFBundleVersion</key>
16+
<string>1</string>
17+
<key>NSExtension</key>
18+
<dict>
19+
<key>NSExtensionAttributes</key>
20+
<dict>
21+
<key>QLIsDataBasedPreview</key>
22+
<true/>
23+
<key>QLSupportedContentTypes</key>
24+
<array>
25+
<string>org.musescore.mscz</string>
26+
</array>
27+
<key>QLSupportsSearchableItems</key>
28+
<false/>
29+
</dict>
30+
<key>NSExtensionPointIdentifier</key>
31+
<string>com.apple.quicklook.preview</string>
32+
<key>NSExtensionPrincipalClass</key>
33+
<string>MuseScoreQLPreviewProvider.PreviewProvider</string>
34+
</dict>
35+
</dict>
36+
</plist>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import Cocoa
2+
import Quartz
3+
import ZIPFoundation
4+
import os
5+
6+
enum PreviewError: LocalizedError {
7+
case fileNotFound(String)
8+
case invalidData
9+
10+
var errorDescription: String? {
11+
switch self {
12+
case .fileNotFound(let what):
13+
return "\(what) not found"
14+
case .invalidData:
15+
return "Invalid or empty data"
16+
}
17+
}
18+
}
19+
20+
class PreviewProvider: QLPreviewProvider, QLPreviewingController {
21+
22+
let logger = Logger()
23+
24+
func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply {
25+
logger.info("providing QL preview for \(request.fileURL.absoluteString, privacy: .public)")
26+
27+
let pdf = try readFileFromZip(at: request.fileURL, fileName: "preview.pdf")
28+
logger.info("found a \(pdf.count) byte pdf")
29+
30+
guard let doc = PDFDocument(data: pdf) else {
31+
throw PreviewError.fileNotFound("valid PDF document")
32+
}
33+
34+
guard let firstPage = doc.page(at: 0) else {
35+
throw PreviewError.fileNotFound("first page in PDF")
36+
}
37+
38+
let pageBounds = firstPage.bounds(for: .mediaBox)
39+
let size = CGSize(width: pageBounds.width, height: pageBounds.height)
40+
41+
logger.info("PDF page dimensions: \(size.width, privacy: .public) x \(size.height, privacy: .public)")
42+
43+
return QLPreviewReply(forPDFWithPageSize: size) { _ in doc }
44+
}
45+
46+
func readFileFromZip(at zipFileURL: URL, fileName: String) throws -> Data {
47+
let archive = try Archive(url: zipFileURL, accessMode: .read)
48+
guard let entry = archive[fileName] else {
49+
throw PreviewError.fileNotFound("\(fileName) in ZIP archive")
50+
}
51+
52+
var data = Data()
53+
let _ = try archive.extract(entry) { data.append($0) }
54+
55+
guard !data.isEmpty else {
56+
throw PreviewError.invalidData
57+
}
58+
59+
return data
60+
}
61+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleIdentifier</key>
6+
<string>org.musescore.MuseScoreThumbnailProvider</string>
7+
<key>CFBundleName</key>
8+
<string>MuseScoreThumbnailProvider</string>
9+
<key>CFBundleExecutable</key>
10+
<string>MuseScoreThumbnailProvider</string>
11+
<key>CFBundlePackageType</key>
12+
<string>XPC!</string>
13+
<key>CFBundleShortVersionString</key>
14+
<string>1.0</string>
15+
<key>CFBundleVersion</key>
16+
<string>1</string>
17+
<key>NSExtension</key>
18+
<dict>
19+
<key>NSExtensionAttributes</key>
20+
<dict>
21+
<key>QLSupportedContentTypes</key>
22+
<array>
23+
<string>org.musescore.mscz</string>
24+
</array>
25+
<key>QLThumbnailMinimumDimension</key>
26+
<integer>0</integer>
27+
</dict>
28+
<key>NSExtensionPointIdentifier</key>
29+
<string>com.apple.quicklook.thumbnail</string>
30+
<key>NSExtensionPrincipalClass</key>
31+
<string>MuseScoreThumbnailProvider.ThumbnailProvider</string>
32+
</dict>
33+
</dict>
34+
</plist>

0 commit comments

Comments
 (0)