Slide 1

Slide 1 text

Building a macOS screen saver with Kotlin Márton Braun Developer Advocate JetBrains @zsmb13

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

class KotlinLogosView : ScreenSaverView { }

Slide 10

Slide 10 text

class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } }

Slide 11

Slide 11 text

class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }

Slide 12

Slide 12 text

class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func animateOneFrame() { super.animateOneFrame() } }

Slide 13

Slide 13 text

class KotlinLogosView : ScreenSaverView { override init?(frame: NSRect, isPreview: Bool) { super.init(frame: frame, isPreview: isPreview) animationTimeInterval = 1 / 60.0 } required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func animateOneFrame() { super.animateOneFrame() } override var hasConfigureSheet: Bool { return false } override var configureSheet: NSWindow? { return nil } }

Slide 14

Slide 14 text

import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework plugins { kotlin("multiplatform") version "2.1.10" } kotlin { val xcf = XCFramework("KotlinLogo") val targets = listOf(macosX64(), macosArm64()) targets.forEach { target -> target.binaries.framework { binaryOption("bundleId", "co.zsmb.KotlinLogos") baseName = "KotlinLogo" isStatic = true xcf.add(this) } } }

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Xcode project Gradle project ScreenSaverView KotlinScreenSaverView

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

let bundle: NSBundle = Bundle(for: type(of: self))

Slide 19

Slide 19 text

let bundle: NSBundle = Bundle(for: type(of: self)) val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") import platform.Foundation.NSBundle

Slide 20

Slide 20 text

let bundle: NSBundle = Bundle(for: type(of: self)) let image: NSImage = bundle.image(forResource: "kotlin0") val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") import platform.Foundation.NSBundle

Slide 21

Slide 21 text

let bundle: NSBundle = Bundle(for: type(of: self)) let image: NSImage = bundle.image(forResource: "kotlin0") val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") val image: NSImage = bundle.imageForResource("kotlin0") import platform.AppKit.NSImage import platform.AppKit.imageForResource import platform.Foundation.NSBundle

Slide 22

Slide 22 text

let bundle: NSBundle = Bundle(for: type(of: self)) let image: NSImage = bundle.image(forResource: "kotlin0") val bundle: NSBundle = NSBundle.bundleWithIdentifier("co.zsmb.KotlinLogos") val image: NSImage = bundle.imageForResource("kotlin0") import platform.AppKit.NSImage import platform.AppKit.imageForResource import platform.Foundation.NSBundle

Slide 23

Slide 23 text

import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView)

Slide 24

Slide 24 text

import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView)

Slide 25

Slide 25 text

import platform.AppKit.NSImageView import platform.Foundation.NSMakeRect val imageView = NSImageView().apply { image = logoImage frame = NSMakeRect(x = 300.0, y = 300.0, w = 200.0, h = 200.0) } screenSaverView.addSubview(imageView) val frame = NSMakeRect(x = newX, y = newY, w = 200.0, h = 200.0) imageView.frame = frame

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

1 2 3 4 5 6 7

Slide 28

Slide 28 text

Delete your old output files before running a new build Build your project Go to Activity Monitor and force quit all legacyScreenSaver processes Go to System Settings, choose any other screen saver Delete your custom screen saver from System Settings Quit System Settings Install your new screen saver 1 2 3 4 5 6 7

Slide 29

Slide 29 text

Build your project Go to Activity Monitor and force quit all legacyScreenSaver processes Go to System Settings, choose any other screen saver Delete your custom screen saver from System Settings Quit System Settings Install your new screen saver 2 3 4 5 6 7

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty { private val userDefaults = NSUserDefaults() override fun getValue(!!..., property: KProperty!!<*>): Long { return userDefaults.objectForKey(property.name) as? Long !?: 0L } override fun setValue(!!..., property: KProperty!!<*>, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }

Slide 33

Slide 33 text

import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty { private val userDefaults = NSUserDefaults() override fun getValue(!!..., property: KProperty!!<*>): Long { return userDefaults.objectForKey(property.name) as? Long !?: 0L } override fun setValue(!!..., property: KProperty!!<*>, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }

Slide 34

Slide 34 text

import platform.Foundation.NSUserDefaults class LongUserDefaultDelegate : ReadWriteProperty { private val userDefaults = NSUserDefaults() override fun getValue(!!..., property: KProperty!!<*>): Long { return userDefaults.objectForKey(property.name) as? Long !?: 0L } override fun setValue(!!..., property: KProperty!!<*>, value: Long) { userDefaults.setInteger(value, forKey = property.name) } } object Preferences { var LOGO_SET by LongUserDefaultDelegate(0) var LOGO_SIZE by LongUserDefaultDelegate(200) var LOGO_COUNT by LongUserDefaultDelegate(1) var SPEED by LongUserDefaultDelegate(10) }

Slide 35

Slide 35 text

import platform.Foundation.NSNotificationCenter import platform.Foundation.NSUserDefaultsDidChangeNotification NSNotificationCenter.defaultCenter.addObserverForName( name = NSUserDefaultsDidChangeNotification ) { reinitialize() }

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

import platform.AppKit.NSModalResponseOK import platform.AppKit.NSOpenPanel import platform.Foundation.NSURL val openPanel = NSOpenPanel().apply { allowsMultipleSelection = false canChooseDirectories = true canCreateDirectories = false canChooseFiles = false } openPanel.beginWithCompletionHandler { response -> if (response !== NSModalResponseOK !&& openPanel.URLs.isNotEmpty()) { val url = openPanel.URLs.first() as NSURL !!... } }

Slide 40

Slide 40 text

import platform.Foundation.NSDirectoryEnumerationSkipsHiddenFiles import platform.Foundation.NSDirectoryEnumerationSkipsSubdirectoryDes import platform.Foundation.NSFileManager import platform.Foundation.NSURL val enum = NSFileManager.defaultManager.enumeratorAtURL( url = NSURL(string = folderUrl), includingPropertiesForKeys = null, options = NSDirectoryEnumerationSkipsHiddenFiles or NSDirectoryEnumerationSkipsSubdirectoryDescendants, errorHandler = null ) var next = enum.nextObject() while (next !!= null) { !// !!... next = enum.nextObject() }

Slide 41

Slide 41 text

import platform.Foundation.NSDirectoryEnumerationSkipsHiddenFiles import platform.Foundation.NSDirectoryEnumerationSkipsSubdirectoryDes import platform.Foundation.NSFileManager import platform.Foundation.NSURL val enum = NSFileManager.defaultManager.enumeratorAtURL( url = NSURL(string = folderUrl), includingPropertiesForKeys = null, options = NSDirectoryEnumerationSkipsHiddenFiles or NSDirectoryEnumerationSkipsSubdirectoryDescendants, errorHandler = null ) var next = enum.nextObject() while (next !!= null) { !// !!... next = enum.nextObject() }

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

legacyScreenSaver ScreenSaverView

Slide 48

Slide 48 text

legacyScreenSaver ScreenSaverView

Slide 49

Slide 49 text

legacyScreenSaver ScreenSaverView ScreenSaverView

Slide 50

Slide 50 text

legacyScreenSaver ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverView ScreenSaverVie ScreenSaverVi ScreenSaverV ScreenSaver ScreenSav ScreenSa ScreenS Screen

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

class KotlinLogosView: ScreenSaverView { @objc func willStop(_ aNotification: Notification) { if (!isPreview) { self.removeFromSuperview() } } }

Slide 55

Slide 55 text

class KotlinLogosView: ScreenSaverView { @objc func willStop(_ aNotification: Notification) { if (!isPreview) { NSApplication.shared.terminate(nil) } } }

Slide 56

Slide 56 text

Do not try building a macOS screen saver

Slide 57

Slide 57 text

Do not try building a macOS screen saver Don’t be afraid to use interop with Kotlin/Native

Slide 58

Slide 58 text

Do not try building a macOS screen saver Don’t be afraid to use interop with Kotlin/Native Put some Kotlin logos on your machine!

Slide 59

Slide 59 text

Why isn’t this screensaver built with ?!

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

Do not try building a macOS screen saver Don’t be afraid to use interop with Kotlin/Native Put some Kotlin logos on your machine! Maybe build a cool screen saver with Compose?

Slide 62

Slide 62 text

… and don’t forget to vote! github.com/zsmb13/KotlinLogo-ScreenSaver/ zsmb.co/talks Márton Braun @zsmb13.co