diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt index 2a51e0ab1b..f59b838b6a 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/JvmApplicationDistributions.kt @@ -34,12 +34,13 @@ abstract class JvmApplicationDistributions : AbstractDistributions() { fn.execute(windows) } + @JvmOverloads fun fileAssociation( mimeType: String, extension: String, description: String, - linuxIcon: File? = null, windowsIcon: File? = null, macOSIcon: File? = null, + linuxIconFile: File? = null, windowsIconFile: File? = null, macOSIconFile: File? = null, ) { - linux.fileAssociation(mimeType, extension, description, linuxIcon) - windows.fileAssociation(mimeType, extension, description, windowsIcon) - macOS.fileAssociation(mimeType, extension, description, macOSIcon) + linux.fileAssociation(mimeType, extension, description, linuxIconFile) + windows.fileAssociation(mimeType, extension, description, windowsIconFile) + macOS.fileAssociation(mimeType, extension, description, macOSIconFile) } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt index 0d86404354..d70ca65adf 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/dsl/PlatformSettings.kt @@ -20,6 +20,8 @@ abstract class AbstractPlatformSettings { var installationPath: String? = null internal val fileAssociations: MutableSet = mutableSetOf() + + @JvmOverloads fun fileAssociation(mimeType: String, extension: String, description: String, iconFile: File? = null) { fileAssociations.add(FileAssociation(mimeType, extension, description, iconFile)) } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt index e342fb3ab4..0a124b0575 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/tasks/AbstractJPackageTask.kt @@ -253,19 +253,33 @@ abstract class AbstractJPackageTask @Inject constructor( internal val fileAssociations: SetProperty = objects.setProperty(FileAssociation::class.java) private val iconMapping by lazy { - val icons = fileAssociations.orNull.orEmpty().mapNotNull { it.iconFile } + val icons = fileAssociations.orNull.orEmpty().mapNotNull { it.iconFile }.distinct() if (icons.isEmpty()) return@lazy emptyMap() - val iconTempNames = generateSequence { - icons.mapTo(mutableSetOf()) { String(CharArray(10) { ('a'..'z').random() }) } - }.first { it.size == icons.size } + val iconTempNames: List = mutableListOf().apply { + val usedNames = mutableSetOf("${packageName.get()}.icns") + for (icon in icons) { + if (!icon.exists()) continue + if (usedNames.add(icon.name)) { + add(icon.name) + continue + } + val nameWithoutExtension = icon.nameWithoutExtension + val extension = icon.extension + for (n in 1UL..ULong.MAX_VALUE) { + val newName = "$nameWithoutExtension ($n).$extension" + if (usedNames.add(newName)) { + add(newName) + break + } + } + } + } val appDir = destinationDir.ioFile.resolve("${packageName.get()}.app") val iconsDir = appDir.resolve("Contents").resolve("Resources") if (iconsDir.exists()) { iconsDir.deleteRecursively() } - icons.zip(iconTempNames) { icon, newName -> - icon to iconsDir.resolve(newName + icon.name.drop(icon.nameWithoutExtension.length)) - }.toMap() + icons.zip(iconTempNames) { icon, newName -> icon to iconsDir.resolve(newName) }.toMap() } private lateinit var jvmRuntimeInfo: JvmRuntimeProperties diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist index b9a58314b0..9f703cf88c 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist +++ b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Expected-Info.plist @@ -32,12 +32,87 @@ true NSHighResolutionCapable true + CFBundleDocumentTypes + + + CFBundleTypeRole + Editor + CFBundleTypeExtensions + + kot + + CFBundleTypeIconFile + Kotlin_icon_big.icns + CFBundleTypeMIMETypes + text/kotlin + CFBundleTypeName + Kotlin Source File0 + CFBundleTypeOSTypes + + **** + + + + CFBundleTypeRole + Editor + CFBundleTypeExtensions + + kot1 + + CFBundleTypeIconFile + TestPackage.icns + CFBundleTypeMIMETypes + text/kotlin + CFBundleTypeName + Kotlin Source File1 + CFBundleTypeOSTypes + + **** + + + + CFBundleTypeRole + Editor + CFBundleTypeExtensions + + kott + + CFBundleTypeIconFile + Kotlin_icon_big (1).icns + CFBundleTypeMIMETypes + text/kotlin + CFBundleTypeName + Kotlin Source File2 + CFBundleTypeOSTypes + + **** + + + + CFBundleTypeRole + Editor + CFBundleTypeExtensions + + kott1 + + CFBundleTypeIconFile + TestPackage.icns + CFBundleTypeMIMETypes + text/kotlin + CFBundleTypeName + Kotlin Source File3 + CFBundleTypeOSTypes + + **** + + + CFBundleURLTypes CFBundleURLName - Exameple URL + Example URL CFBundleURLSchemes exampleUrl diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.icns b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.icns new file mode 100644 index 0000000000..fedf6a3f2f Binary files /dev/null and b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.icns differ diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.ico b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.ico new file mode 100644 index 0000000000..3e9c11bb6d Binary files /dev/null and b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.ico differ diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.png b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.png new file mode 100644 index 0000000000..1f93755e75 Binary files /dev/null and b/gradle-plugins/compose/src/test/test-projects/application/macOptions/Kotlin_icon_big.png differ diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle b/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle index 73ccc33a39..31490dd7e0 100644 --- a/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle +++ b/gradle-plugins/compose/src/test/test-projects/application/macOptions/build.gradle @@ -15,7 +15,7 @@ def extraInfoPlistKeys = """ CFBundleURLName - Exameple URL + Example URL CFBundleURLSchemes exampleUrl @@ -28,12 +28,36 @@ compose.desktop { mainClass = "MainKt" nativeDistributions { packageName = "TestPackage" + fileAssociation( + "text/kotlin", + "kot", + "Kotlin Source File0", + project.file("Kotlin_icon_big.png"), + project.file("Kotlin_icon_big.ico"), + project.file("Kotlin_icon_big.icns"), + ) + fileAssociation( + "text/kotlin", + "kot1", + "Kotlin Source File1", + ) macOS { dockName = "CustomDockName" minimumSystemVersion = "12.0" infoPlist { extraKeysRawXml = extraInfoPlistKeys } + fileAssociation( + "text/kotlin", + "kott", + "Kotlin Source File2", + project.file("subdir/Kotlin_icon_big.icns"), + ) + fileAssociation( + "text/kotlin", + "kott1", + "Kotlin Source File3", + ) } } } diff --git a/gradle-plugins/compose/src/test/test-projects/application/macOptions/subdir/Kotlin_icon_big.icns b/gradle-plugins/compose/src/test/test-projects/application/macOptions/subdir/Kotlin_icon_big.icns new file mode 100644 index 0000000000..fedf6a3f2f Binary files /dev/null and b/gradle-plugins/compose/src/test/test-projects/application/macOptions/subdir/Kotlin_icon_big.icns differ diff --git a/tutorials/Native_distributions_and_local_execution/README.md b/tutorials/Native_distributions_and_local_execution/README.md index ffc91c4904..3152bb348d 100755 --- a/tutorials/Native_distributions_and_local_execution/README.md +++ b/tutorials/Native_distributions_and_local_execution/README.md @@ -511,6 +511,60 @@ compose.desktop { } ``` +## File associations + +File associations can be added with `fileAssociation` function. +These associations can optionally include custom icons. +Similar to [app icon](#app-icon), these custom icons also have OS-specific formats: `.icns` for macOS, `.ico` for Windows and `.png` for Linux. + +It is possible to specify both OS-specific and OS-agnostic file associations. +For OS-agnostic you can specify icon files for all three OSes in-place, +while for OS-specific there is a shorthand with a single icon file parameter. + +```kotlin +compose.desktop { + application { + mainClass = "MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + + packageName = "file-associations-demo" + packageVersion = "1.0.0" + + macOS { + fileAssociation( + mimeType = "text/kotlin", + extension = "kott", + description = "Kotlin Source File", + iconFile = project.file("Kotlin_icon_big.icns"), + ) + fileAssociation( + mimeType = "text/kotlin", + extension = "ko", + description = "Kotlin Source File", + ) + } + + fileAssociation( + mimeType = "text/kotlin", + extension = "kot", + description = "Kotlin Source File", + macOSIconFile = project.file("Kotlin_icon_big.icns"), + windowsIconFile = project.file("Kotlin_icon_big.ico"), + linuxIconFile = project.file("Kotlin_icon_big.png"), + ) + + fileAssociation( + mimeType = "text/kotlin", + extension = "kottt", + description = "Kotlin Source File", + ) + } + } +} +``` + ## Customizing Info.plist on macOS We aim to support important platform-specific customization use-cases via declarative DSL.