diff --git a/samples/SkiaMultiplatformSample/build.gradle.kts b/samples/SkiaMultiplatformSample/build.gradle.kts index 2d41ca8c5..a0c7085fe 100644 --- a/samples/SkiaMultiplatformSample/build.gradle.kts +++ b/samples/SkiaMultiplatformSample/build.gradle.kts @@ -219,7 +219,8 @@ tasks.withType().configureEach enum class Target(val simulator: Boolean, val key: String) { WATCHOS_X86(true, "watchos"), WATCHOS_ARM64(false, "watchos"), - IOS_X64(true, "iosX64"), IOS_ARM64(false, "iosArm64") + IOS_X64(true, "iosX64"), IOS_ARM64(false, "iosArm64"), + MACOS_X64(false, "macosX64") } @@ -229,6 +230,7 @@ if (hostOs == "macos") { val target = sdkName.orEmpty().let { when { + it.startsWith("macosx") -> Target.MACOS_X64 it.startsWith("iphoneos") -> Target.IOS_ARM64 it.startsWith("iphonesimulator") -> Target.IOS_X64 it.startsWith("watchos") -> Target.WATCHOS_ARM64 diff --git a/samples/SkiaMultiplatformSample/gradle.properties b/samples/SkiaMultiplatformSample/gradle.properties index 9f8ac5f94..1d5a6613c 100644 --- a/samples/SkiaMultiplatformSample/gradle.properties +++ b/samples/SkiaMultiplatformSample/gradle.properties @@ -1,3 +1,4 @@ kotlin.code.style=official kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false +kotlin.native.binary.memoryModel=experimental diff --git a/samples/SkiaMultiplatformSample/plists/MacOS/Info.plist b/samples/SkiaMultiplatformSample/plists/MacOS/Info.plist new file mode 100644 index 000000000..aa735eab0 --- /dev/null +++ b/samples/SkiaMultiplatformSample/plists/MacOS/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + UILaunchStoryboardName + + NSHighResolutionCapable + True + + diff --git a/samples/SkiaMultiplatformSample/project.yml b/samples/SkiaMultiplatformSample/project.yml index 77f7c8d52..0d56e2e60 100644 --- a/samples/SkiaMultiplatformSample/project.yml +++ b/samples/SkiaMultiplatformSample/project.yml @@ -27,3 +27,19 @@ targets: ENABLE_BITCODE: "YES" ONLY_ACTIVE_ARCH: "NO" VALID_ARCHS: "arm64" + SkikoSampleMacOS: + type: application + platform: macOS + deploymentTarget: "11.5" + prebuildScripts: + - script: cd "$SRCROOT" && ./gradlew -p . packForXcode + name: GradleCompile + info: + path: plists/MacOS/Info.plist + properties: + UILaunchStoryboardName: "" + sources: + - "src/" + settings: + LIBRARY_SEARCH_PATHS: "$(inherited)" + ENABLE_BITCODE: "YES" \ No newline at end of file diff --git a/samples/SkiaMultiplatformSample/src/commonMain/kotlin/org/jetbrains/skiko/sample/Clocks.kt b/samples/SkiaMultiplatformSample/src/commonMain/kotlin/org/jetbrains/skiko/sample/Clocks.kt index b16f60735..caf137f92 100644 --- a/samples/SkiaMultiplatformSample/src/commonMain/kotlin/org/jetbrains/skiko/sample/Clocks.kt +++ b/samples/SkiaMultiplatformSample/src/commonMain/kotlin/org/jetbrains/skiko/sample/Clocks.kt @@ -11,7 +11,7 @@ import kotlin.math.sin import kotlin.math.PI import kotlin.math.pow -class Clocks(private val layer: SkiaLayer): SkikoView { +open class Clocks(private val layer: SkiaLayer): SkikoView { private val platformYOffset = if (hostOs == OS.Ios) 50f else 5f private var frame = 0 private var xpos = 0.0 diff --git a/samples/SkiaMultiplatformSample/src/jsMain/kotlin/org/jetbrains/skiko/sample/App.js.kt b/samples/SkiaMultiplatformSample/src/jsMain/kotlin/org/jetbrains/skiko/sample/App.js.kt index 41ddf31b8..b4d68e657 100644 --- a/samples/SkiaMultiplatformSample/src/jsMain/kotlin/org/jetbrains/skiko/sample/App.js.kt +++ b/samples/SkiaMultiplatformSample/src/jsMain/kotlin/org/jetbrains/skiko/sample/App.js.kt @@ -1,8 +1,11 @@ package org.jetbrains.skiko.sample import kotlinx.browser.document +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch import org.jetbrains.skiko.GenericSkikoView import org.jetbrains.skiko.SkiaLayer +import org.jetbrains.skiko.notifications.Notification import org.jetbrains.skiko.onContentScaleChanged import org.jetbrains.skiko.wasm.onWasmReady import org.w3c.dom.HTMLCanvasElement @@ -17,5 +20,12 @@ fun main() { canvas.setAttribute("tabindex", "0") skiaLayer.attachTo(canvas) skiaLayer.needRedraw() + + GlobalScope.launch { + Notification( + title = "Hello", + body = "It works", + ).send() + } } } diff --git a/samples/SkiaMultiplatformSample/src/macosMain/kotlin/org/jetbrains/skiko/sample/App.macos.kt b/samples/SkiaMultiplatformSample/src/macosMain/kotlin/org/jetbrains/skiko/sample/App.macos.kt index c8f4c2d46..f734f3fec 100644 --- a/samples/SkiaMultiplatformSample/src/macosMain/kotlin/org/jetbrains/skiko/sample/App.macos.kt +++ b/samples/SkiaMultiplatformSample/src/macosMain/kotlin/org/jetbrains/skiko/sample/App.macos.kt @@ -1,13 +1,28 @@ package org.jetbrains.skiko.sample +import kotlinx.coroutines.* import platform.AppKit.* import org.jetbrains.skiko.* -import platform.Foundation.NSMakeRect -import platform.Foundation.NSSelectorFromString -import platform.darwin.NSObject +import org.jetbrains.skiko.notifications.Notification +import platform.Foundation.* +import platform.darwin.* -fun makeApp(skiaLayer: SkiaLayer) = Clocks(skiaLayer) +fun makeApp(skiaLayer: SkiaLayer) = object : Clocks(skiaLayer) { + override fun onKeyboardEvent(event: SkikoKeyboardEvent) { + super.onKeyboardEvent(event) + if (event.kind == SkikoKeyboardEventKind.DOWN) when (event.key) { + SkikoKey.KEY_N -> CoroutineScope(SkikoDispatchers.Main).launch { + Notification( + title = "Hello", + body = "It works!" + ).send() + println("After notification sent") + } + else -> {} + } + } +} fun main() { val app = NSApplication.sharedApplication() @@ -20,7 +35,7 @@ fun main() { appMenuItem.setSubmenu(appMenu) appMenu.addItemWithTitle("About $appName", NSSelectorFromString("orderFrontStandardAboutPanel:"), "a") appMenu.addItemWithTitle("Quit $appName", NSSelectorFromString("terminate:"), "q") - + app.delegate = object: NSObject(), NSApplicationDelegateProtocol { override fun applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication): Boolean { return true diff --git a/skiko/gradle.properties b/skiko/gradle.properties index fee9bcc3a..541e4bfee 100644 --- a/skiko/gradle.properties +++ b/skiko/gradle.properties @@ -15,3 +15,5 @@ dependencies.skia.android-x64=m99-51988d0bc1 dependencies.skia.android-arm64=m99-51988d0bc1 org.gradle.jvmargs=-Xmx3G + +kotlin.native.binary.memoryModel=experimental \ No newline at end of file diff --git a/skiko/src/awtMain/kotlin/org/jetbrains/skiko/notifications/Expects.awt.kt b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/notifications/Expects.awt.kt new file mode 100644 index 000000000..f286907fa --- /dev/null +++ b/skiko/src/awtMain/kotlin/org/jetbrains/skiko/notifications/Expects.awt.kt @@ -0,0 +1,5 @@ +package org.jetbrains.skiko.notifications + +internal actual suspend fun sendNotification(notification: Notification) { + TODO("Not implemented yet") +} diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Exceptions.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Exceptions.kt new file mode 100644 index 000000000..316e4f430 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Exceptions.kt @@ -0,0 +1,6 @@ +package org.jetbrains.skiko.notifications + +sealed class NotificationError(override val message: String) : Throwable(message) + +class PermissionNotGrantedError : NotificationError("Permission not granted for notification") +class NotificationsNotSupportedError : NotificationError("Notifications are not supported for this platform") \ No newline at end of file diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Expects.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Expects.kt new file mode 100644 index 000000000..8e0ecce98 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Expects.kt @@ -0,0 +1,3 @@ +package org.jetbrains.skiko.notifications + +internal expect suspend fun sendNotification(notification: Notification) \ No newline at end of file diff --git a/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Notification.kt b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Notification.kt new file mode 100644 index 000000000..4ac665c71 --- /dev/null +++ b/skiko/src/commonMain/kotlin/org/jetbrains/skiko/notifications/Notification.kt @@ -0,0 +1,7 @@ +package org.jetbrains.skiko.notifications + +class Notification(var title: String, var body: String) { + var iconPath: String? = null + + suspend fun send() = sendNotification(this) +} diff --git a/skiko/src/darwinMain/kotlin/org/jetbrains/skiko/notifications/Expects.darwin.kt b/skiko/src/darwinMain/kotlin/org/jetbrains/skiko/notifications/Expects.darwin.kt new file mode 100644 index 000000000..3fb55cdca --- /dev/null +++ b/skiko/src/darwinMain/kotlin/org/jetbrains/skiko/notifications/Expects.darwin.kt @@ -0,0 +1,44 @@ +package org.jetbrains.skiko.notifications + +import kotlinx.coroutines.withContext +import org.jetbrains.skiko.SkikoDispatchers +import platform.Foundation.NSError +import platform.Foundation.NSUUID +import platform.UserNotifications.* +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@Suppress("NAME_SHADOWING") +internal actual suspend fun sendNotification(notification: Notification) = withContext(SkikoDispatchers.IO) { + val nc = UNUserNotificationCenter.currentNotificationCenter() + val options = UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge + + val sent = suspendCoroutine { cont -> + nc.requestAuthorizationWithOptions(options) { granted: Boolean, err: NSError? -> + if (granted) { + with(notification) { + val request = UNNotificationRequest.requestWithIdentifier(id, content, null) + nc.addNotificationRequest(request) { err: NSError? -> + err?.let { println("Sent with error: $it") } + cont.resume(err == null) + } + } + } else { + println("No auth: $err") + cont.resume(false) + } + } + } + println("Sent: $sent") +} + +private val Notification.content by lazy { + UNMutableNotificationContent().apply { + setBody(body) + setTitle(title) + } +} + +private val Notification.id by lazy { + NSUUID().UUIDString +} \ No newline at end of file diff --git a/skiko/src/jsMain/kotlin/org/jetbrains/skiko/notifications/Expects.js.kt b/skiko/src/jsMain/kotlin/org/jetbrains/skiko/notifications/Expects.js.kt new file mode 100644 index 000000000..7eb7eb9d6 --- /dev/null +++ b/skiko/src/jsMain/kotlin/org/jetbrains/skiko/notifications/Expects.js.kt @@ -0,0 +1,29 @@ +package org.jetbrains.skiko.notifications + +import kotlinx.browser.window +import kotlinx.coroutines.await +import org.w3c.notifications.DENIED +import org.w3c.notifications.GRANTED +import org.w3c.notifications.NotificationOptions +import org.w3c.notifications.NotificationPermission +import org.w3c.notifications.Notification as WebNotification + +internal actual suspend fun sendNotification(notification: Notification) { + if (window.asDynamic()["Notification"] == undefined) { + throw NotificationsNotSupportedError() + } + + val permission = when (WebNotification.permission) { + NotificationPermission.GRANTED -> NotificationPermission.GRANTED + NotificationPermission.DENIED -> NotificationPermission.DENIED + else -> WebNotification.requestPermission().await() + } + + if (permission == NotificationPermission.GRANTED) { + WebNotification(notification.title, NotificationOptions( + body = notification.body + )) + } else { + throw PermissionNotGrantedError() + } +} diff --git a/skiko/src/linuxMain/kotlin/org/jetbrains/skiko/notifications/Expects.linux.kt b/skiko/src/linuxMain/kotlin/org/jetbrains/skiko/notifications/Expects.linux.kt new file mode 100644 index 000000000..f286907fa --- /dev/null +++ b/skiko/src/linuxMain/kotlin/org/jetbrains/skiko/notifications/Expects.linux.kt @@ -0,0 +1,5 @@ +package org.jetbrains.skiko.notifications + +internal actual suspend fun sendNotification(notification: Notification) { + TODO("Not implemented yet") +}