Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Notifications [WIP] #462

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion samples/SkiaMultiplatformSample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile>().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")
}


Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions samples/SkiaMultiplatformSample/plists/MacOS/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UILaunchStoryboardName</key>
<string></string>
<key>NSHighResolutionCapable</key>
<string>True</string>
</dict>
</plist>
16 changes: 16 additions & 0 deletions samples/SkiaMultiplatformSample/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporarily opened to add custom key binding for macOS

private val platformYOffset = if (hostOs == OS.Ios) 50f else 5f
private var frame = 0
private var xpos = 0.0
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,5 +20,12 @@ fun main() {
canvas.setAttribute("tabindex", "0")
skiaLayer.attachTo(canvas)
skiaLayer.needRedraw()

GlobalScope.launch {
Notification(
title = "Hello",
body = "It works",
).send()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if notifications deserve their own package.

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 -> runBlocking {
Notification(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why runBlocking? Cannot we just run it as suspend on SkikoDispatchers.Main dispatcher?

title = "Hello",
body = "It works!"
).send()
}
else -> {}
}
}
}

fun main() {
val app = NSApplication.sharedApplication()
Expand All @@ -20,7 +34,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.skiko.notifications

internal actual suspend fun sendNotification(notification: Notification) {
TODO("Not implemented yet")
}
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.jetbrains.skiko.notifications

internal expect suspend fun sendNotification(notification: Notification)
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.jetbrains.skiko.notifications

import platform.Foundation.NSError
import platform.Foundation.NSUUID
import platform.UserNotifications.*
import platform.darwin.dispatch_async
import platform.darwin.dispatch_block_t
import platform.darwin.dispatch_get_main_queue
import kotlin.native.concurrent.freeze

internal actual suspend fun sendNotification(notification: Notification) {
val nc = UNUserNotificationCenter.currentNotificationCenter().freeze()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to freeze here, this is Objective-C object.

val sendNotification: dispatch_block_t = {
val callback = { granted: Boolean, err: NSError? ->
if (granted) {
val content = UNMutableNotificationContent().apply {
setBody(notification.body)
setTitle(notification.title)
}
val id = NSUUID().UUIDString
val request = UNNotificationRequest.requestWithIdentifier(id, content, null)
val callback = { _: NSError? -> }
nc.addNotificationRequest(request, callback.freeze())
}
}
val options = UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge
nc.requestAuthorizationWithOptions(options, callback.freeze())
}
dispatch_async(dispatch_get_main_queue(), sendNotification.freeze())
}

Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.skiko.notifications

internal actual suspend fun sendNotification(notification: Notification) {
TODO("Not implemented yet")
}