diff --git a/build.gradle b/build.gradle index 41bbcb67..3eef2a58 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,7 @@ def multiplatformModules = [ "orx-no-clear", "orx-noise", "orx-parameters", + "orx-property-watchers", "orx-shade-styles", "orx-shader-phrases", "orx-shapes", diff --git a/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcher.kt b/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcher.kt index 61373f6d..e35a0d71 100644 --- a/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcher.kt +++ b/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcher.kt @@ -4,109 +4,22 @@ import com.sun.nio.file.SensitivityWatchEventModifier import kotlinx.coroutines.* import mu.KotlinLogging import org.openrndr.Program +import org.openrndr.events.Event import org.openrndr.launch import java.io.File import java.nio.file.FileSystems import java.nio.file.Path import java.nio.file.StandardWatchEventKinds import java.nio.file.WatchKey +import java.util.WeakHashMap import kotlin.concurrent.thread +import kotlin.reflect.KProperty + private val logger = KotlinLogging.logger {} -class FileWatcher(private val program: Program, val file: File, private val onChange: (File) -> Unit) { - val path = file.absoluteFile.toPath() - val parent = path.parent - val key = pathKeys.getOrPut(parent) { - parent.register( - watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY), - SensitivityWatchEventModifier.HIGH - ) - } - val watchers = mutableListOf<() -> Unit>() - - init { - watchThread - watching.getOrPut(path) { - mutableListOf() - }.add(this) - keyPaths.getOrPut(key) { parent } - } - - fun stop() { - watching[path]?.remove(this) - } - - internal fun triggerChange() { - program.launch { - onChange(file) - watchers.forEach { it() } - } - } -} - -private val watchers = mutableMapOf<() -> Any, FileWatcher>() - -fun watchFile(program: Program, file: File, transducer: (File) -> T): () -> T { - var result = transducer(file) - val watcher = FileWatcher(program, file) { - try { - result = transducer(file) - } catch (e: Throwable) { - logger.error(e) { - """exception while transducing file""" - } - } - } - - val function = { - result - } - - @Suppress("UNCHECKED_CAST") - watchers[function as () -> Any] = watcher - return function -} - -/** - * Stops the watcher - */ -fun (() -> T).stop() { - @Suppress("UNCHECKED_CAST") - watchers[this as () -> Any]?.stop() - -} - -/** - * Triggers reload - */ -fun (() -> T).triggerChange() { - @Suppress("UNCHECKED_CAST") - watchers[this as () -> Any]?.triggerChange() -} - - -/** - * add watcher to file watcher - */ -fun (() -> T).watch(transducer: (T) -> R): () -> R { - - var result = transducer(this()) - - @Suppress("USELESS_CAST") - watchers[this as () -> Any?]!!.watchers.add { - result = transducer(this()) - } - - return { result } -} - - -@JvmName("programWatchFile") -fun Program.watchFile(file: File, transducer: (File) -> T): () -> T = watchFile(this, file, transducer) - private val watching = mutableMapOf>() private val pathKeys = mutableMapOf() -private val keyPaths = mutableMapOf() +private val keyPaths = WeakHashMap() private val waiting = mutableMapOf() private val watchService by lazy { @@ -119,13 +32,13 @@ private val watchThread by lazy { while (true) { val key = watchService.take() val path = keyPaths[key] + key.pollEvents().forEach { val contextPath = it.context() as Path val fullPath = path?.resolve(contextPath) fullPath?.let { waiting[fullPath]?.cancel() - waiting[fullPath] = GlobalScope.launch { delay(100) watching[fullPath]?.forEach { w -> @@ -138,3 +51,73 @@ private val watchThread by lazy { } } } + + +class FileWatcher( + val file: File, + private val fileChangedEvent: Event, + requestStopEvent: Event? = null +) { + val path = file.absoluteFile.toPath() + val parent = path.parent + val key = pathKeys.getOrPut(parent) { + parent.register( + watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY), + SensitivityWatchEventModifier.HIGH + ) + } + + init { + watchThread + watching.getOrPut(path) { + mutableListOf() + }.add(this) + keyPaths.getOrPut(key) { parent } + requestStopEvent?.listenOnce { + stop() + } + } + + fun stop() { + synchronized(watching) { + logger.info { "stopping, watcher stop requested" } + watching[path]?.remove(this) + } + } + + internal fun triggerChange() { + fileChangedEvent.trigger(file) + } +} + + + +fun watchFile( + file: File, + contentsChangedEvent: Event? = null, + requestStopEvent: Event? = null, + transducer: (File) -> T +): () -> T { + var result = transducer(file) + val fileChangedEvent = Event() + val watcher = FileWatcher(file, fileChangedEvent, requestStopEvent) + + fileChangedEvent.listen { + try { + result = transducer(file) + contentsChangedEvent?.trigger(result) + } catch (e: Throwable) { + logger.error(e) { + """exception while transducing file""" + } + } + } + return { + result + } +} + + +//@JvmName("programWatchFile") +//fun Program.watchFile(file: File, onChange: Event? = null, transducer: (File) -> T): () -> T = +// watchFile(this, file, onChange, transducer = transducer) diff --git a/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcherDelegate.kt b/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcherDelegate.kt new file mode 100644 index 00000000..54453a70 --- /dev/null +++ b/orx-jvm/orx-file-watcher/src/main/kotlin/FileWatcherDelegate.kt @@ -0,0 +1,38 @@ +import kotlinx.coroutines.yield +import org.openrndr.Program +import org.openrndr.events.Event +import org.openrndr.extra.filewatcher.watchFile +import org.openrndr.launch +import java.io.File +import kotlin.reflect.KProperty + +class FileWatcherDelegate( + program: Program, + file: File, + valueChangedEvent: Event? = null, + requestStopEvent: Event? = null, + transducer: (File) -> T +) { + val watchValue = watchFile(file, valueChangedEvent, requestStopEvent, transducer) + var value = watchValue() + + init { + program.launch { + while (true) { + value = watchValue() + yield() + } + } + } + + operator fun getValue(any: Any, property: KProperty<*>): T { + return value + } +} + +fun Program.watchingFile( + file: File, + valueChangedEvent: Event? = null, + requestStopEvent: Event? = null, + transducer: (File) -> R +) = FileWatcherDelegate(this, file, valueChangedEvent, requestStopEvent, transducer) diff --git a/orx-jvm/orx-gui/build.gradle.kts b/orx-jvm/orx-gui/build.gradle.kts index 265937ac..ef2bbb85 100644 --- a/orx-jvm/orx-gui/build.gradle.kts +++ b/orx-jvm/orx-gui/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(project(":orx-parameters")) api(project(":orx-jvm:orx-panel")) api(project(":orx-noise")) + demoImplementation(project(":orx-property-watchers")) implementation(libs.openrndr.application) implementation(libs.openrndr.math) implementation(libs.openrndr.filter) diff --git a/orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt b/orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt new file mode 100644 index 00000000..0be17a89 --- /dev/null +++ b/orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt @@ -0,0 +1,28 @@ +import org.openrndr.application +import org.openrndr.extra.gui.GUI +import org.openrndr.extra.parameters.Description +import org.openrndr.extra.parameters.PathParameter +import org.openrndr.extra.propertywatchers.watchingImagePath + +fun main() { + application { + program { + val gui = GUI() + gui.compartmentsCollapsedByDefault = false + + val settings = @Description("Settings") object { + @PathParameter("image", extensions = ["jpg", "png"], order = 10) + var imagePath = "demo-data/image-001.png" + + val image by watchingImagePath(::imagePath) { + it + } + } + gui.add(settings) + extend(gui) + extend { + drawer.image(settings.image) + } + } + } +} \ No newline at end of file diff --git a/orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt b/orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt index 889ed862..13b99df9 100644 --- a/orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt +++ b/orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt @@ -11,12 +11,6 @@ import org.openrndr.shape.Circle */ fun main() = application { program { - // -- this block is for automation purposes only - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } - } val gui = GUI() gui.compartmentsCollapsedByDefault = false diff --git a/orx-jvm/orx-gui/src/main/kotlin/Gui.kt b/orx-jvm/orx-gui/src/main/kotlin/Gui.kt index be676ab3..a141618c 100644 --- a/orx-jvm/orx-gui/src/main/kotlin/Gui.kt +++ b/orx-jvm/orx-gui/src/main/kotlin/Gui.kt @@ -1,14 +1,12 @@ package org.openrndr.extra.gui import com.google.gson.Gson +import com.google.gson.JsonSyntaxException import com.google.gson.reflect.TypeToken import mu.KotlinLogging import org.openrndr.* import org.openrndr.color.ColorRGBa -import org.openrndr.dialogs.getDefaultPathForContext -import org.openrndr.dialogs.openFileDialog -import org.openrndr.dialogs.saveFileDialog -import org.openrndr.dialogs.setDefaultPathForContext +import org.openrndr.dialogs.* import org.openrndr.draw.Drawer import org.openrndr.extra.noise.Random import org.openrndr.extra.noise.random @@ -58,7 +56,7 @@ private fun getPersistedOrDefault( compartmentLabel: String, property: KMutableProperty1, obj: Any -): T? { +): T { val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel] if (state == null) { return property.get(obj) @@ -83,7 +81,7 @@ class GUIAppearance(val baseColor: ColorRGBa = ColorRGBa.GRAY, val barWidth: Int class GUI( val appearance: GUIAppearance = GUIAppearance(), val defaultStyles: List = defaultStyles(), - ) : Extension { +) : Extension { private var onChangeListener: ((name: String, value: Any?) -> Unit)? = null override var enabled = true @@ -593,6 +591,47 @@ class GUI( } } + ParameterType.Path -> { + button { + label = "Load ${parameter.label}" + clicked { + + if (parameter.pathIsDirectory == false) { + openFileDialog( + supportedExtensions = parameter.pathExtensions?.toList() ?: emptyList(), + contextID = parameter.pathContext ?: "null" + ) { + val resolvedPath = if (parameter.absolutePath == true) { + it.absolutePath + } else { + it.relativeTo(File(".").absoluteFile).path + } + setAndPersist( + compartment.label, + parameter.property as KMutableProperty1, + obj, + resolvedPath + ) + } + } else { + openFolderDialog(contextID = parameter.pathContext ?: "null") { + val resolvedPath = if (parameter.absolutePath == true) { + it.absolutePath + } else { + it.relativeTo(File(".").absoluteFile).path + } + setAndPersist( + compartment.label, + parameter.property as KMutableProperty1, + obj, + resolvedPath + ) + } + } + } + } + } + ParameterType.DoubleList -> { sequenceEditor { range = parameter.doubleRange!! @@ -731,7 +770,10 @@ class GUI( it.value.data as? Enum<*> ?: error("no data") ) - onChangeListener?.invoke(parameter.property!!.name, it.value.data as? Enum<*> ?: error("no data")) + onChangeListener?.invoke( + parameter.property!!.name, + it.value.data as? Enum<*> ?: error("no data") + ) } getPersistedOrDefault( compartment.label, @@ -838,6 +880,8 @@ class GUI( maxValue = k.doubleRange?.endInclusive ) + ParameterType.Path -> ParameterValue(textValue = k.property.qget(lo.obj) as String) + ParameterType.Option -> ParameterValue(optionValue = (k.property.qget(lo.obj) as Enum<*>).name) } ) @@ -919,6 +963,10 @@ class GUI( parameter.property.enumSet(lo.obj, it) } + ParameterType.Path -> parameterValue.textValue?.let { + parameter.property.qset(lo.obj, it) + } + ParameterType.Action -> { // intentionally do nothing } @@ -933,7 +981,12 @@ class GUI( fun loadParameters(file: File) { val json = file.readText() val typeToken = object : TypeToken>>() {} - val labeledValues: Map> = Gson().fromJson(json, typeToken.type) + val labeledValues: Map> = try { + Gson().fromJson(json, typeToken.type) + } catch (e: JsonSyntaxException) { + println("could not parse json: $json") + throw e; + } fromObject(labeledValues) } @@ -998,6 +1051,10 @@ class GUI( } ?: error("could not find item") } + ParameterType.Path -> { + + } + ParameterType.Action -> { // intentionally do nothing } diff --git a/orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt b/orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt index cd564617..a3c90c46 100644 --- a/orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt +++ b/orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt @@ -12,32 +12,33 @@ fun main() { width = 1280 height = 720 } - oliveProgram(scriptHost = OliveScriptHost.JSR223) { + oliveProgram(scriptHost = OliveScriptHost.JSR223_REUSE) { extend { drawer.clear(ColorRGBa.GRAY) drawer.fill = ColorRGBa.WHITE for (i in 0 until 100) { drawer.circle( - width / 2.0 + cos(seconds + i) * 320.0, - i * 7.2, - cos(i + seconds * 0.5) * 20.0 + 20.0) + width / 2.0 + cos(seconds + i) * 320.0, + i * 7.2, + cos(i + seconds * 0.5) * 20.0 + 20.0 + ) } } } - // -- this is only needed for the automated screenshots - .olive.scriptLoaded.listen { - if (System.getProperty("takeScreenshot") == "true") { - // -- this is a bit of hack, we need to push the screenshot extension in front of the olive one - fun extendHead(extension: T, configure: T.() -> Unit): T { - program.extensions.add(0, extension) - extension.configure() - extension.setup(program) - return extension - } - extendHead(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } + // -- this is only needed for the automated screenshots + .olive.scriptLoaded.listen { + if (System.getProperty("takeScreenshot") == "true") { + // -- this is a bit of hack, we need to push the screenshot extension in front of the olive one + fun extendHead(extension: T, configure: T.() -> Unit): T { + program.extensions.add(0, extension) + extension.configure() + extension.setup(program) + return extension + } + extendHead(SingleScreenshot()) { + this.outputFile = System.getProperty("screenshotPath") } } + } } } \ No newline at end of file diff --git a/orx-jvm/orx-olive/src/main/kotlin/Olive.kt b/orx-jvm/orx-olive/src/main/kotlin/Olive.kt index 5210fd88..1cc0a00a 100644 --- a/orx-jvm/orx-olive/src/main/kotlin/Olive.kt +++ b/orx-jvm/orx-olive/src/main/kotlin/Olive.kt @@ -11,8 +11,6 @@ import org.openrndr.events.Event import org.openrndr.exceptions.stackRootClassName import org.openrndr.extra.kotlinparser.extractProgram import org.openrndr.launch -import org.openrndr.extra.filewatcher.stop -import org.openrndr.extra.filewatcher.triggerChange import org.openrndr.extra.filewatcher.watchFile import java.io.File @@ -63,9 +61,14 @@ class Olive

(val resources: Resources? = null, private var scriptMod * reloads the active script */ fun reload() { - watcher?.triggerChange() + // watcher?.triggerChange() } + class ScriptWatcher + + + + private var watcherRequestStopEvent = Event() private var watcher: (() -> Unit)? = null @OptIn(DelicateCoroutinesApi::class) @@ -99,7 +102,12 @@ class Olive

(val resources: Resources? = null, private var scriptMod val originalAssetProperties = program.assetProperties.toMutableMap() fun setupScript(scriptFile: String) { - watcher?.stop() + if (watcher != null) { + logger.info { "requesting watcher stop" } + watcherRequestStopEvent.trigger(Unit) + } else { + logger.info { "no existing watcher" } + } val f = File(scriptFile) if (!f.exists()) { f.parentFile.mkdirs() @@ -124,7 +132,7 @@ class Olive

(val resources: Resources? = null, private var scriptMod val jsr233ObjectLoader = if (scriptHost == OliveScriptHost.JSR223_REUSE) ScriptObjectLoader() else null - watcher = program.watchFile(File(script)) { + watcher = watchFile(File(script), requestStopEvent = watcherRequestStopEvent) { try { logger.info("change detected, reloading script") @@ -188,7 +196,7 @@ class Olive

(val resources: Resources? = null, private var scriptMod val destFile = File("$dest/${filePath}").absoluteFile - program.watchFile(file) { + watchFile(file) { if (resources[file]!! && filePath != null) { file.copyTo(destFile, overwrite = true) reload() diff --git a/orx-parameters/build.gradle.kts b/orx-parameters/build.gradle.kts index 8c700d19..8d728647 100644 --- a/orx-parameters/build.gradle.kts +++ b/orx-parameters/build.gradle.kts @@ -3,14 +3,7 @@ plugins { } kotlin { - jvm { - testRuns["test"].executionTask { - useJUnitPlatform { - includeEngines("spek2") - } - } - } - sourceSets { + sourceSets { @Suppress("UNUSED_VARIABLE") val commonMain by getting { dependencies { @@ -25,8 +18,6 @@ kotlin { val jvmTest by getting { dependencies { implementation(libs.kluent) - implementation(libs.spek.dsl) - runtimeOnly(libs.spek.junit5) runtimeOnly(libs.kotlin.reflect) } } diff --git a/orx-parameters/src/commonMain/kotlin/Annotations.kt b/orx-parameters/src/commonMain/kotlin/Annotations.kt index ecb92472..3d6644ca 100644 --- a/orx-parameters/src/commonMain/kotlin/Annotations.kt +++ b/orx-parameters/src/commonMain/kotlin/Annotations.kt @@ -159,6 +159,29 @@ annotation class Vector4Parameter( @Retention(AnnotationRetention.RUNTIME) annotation class ActionParameter(val label: String, val order: Int = Int.MAX_VALUE) + +/** + * ActionParameter annotation for functions without arguments + * @property label a short description of the parameter + * @property absolute should the path be stored as an absolute path + * @property context which dialog context to use + * @property extensions an array of supported extensions + * @property directory the path points to a directory + * @property order hint for where to place the parameter in user interfaces + */ + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class PathParameter( + val label: String, + val absolute: Boolean = false, + val context: String = "null", + val extensions: Array = [], + val directory: Boolean = false, + val order: Int = Int.MAX_VALUE +) + + // // enum class ParameterType(val annotationClass: KClass) { @@ -173,7 +196,8 @@ enum class ParameterType(val annotationClass: KClass) { Vector2(Vector2Parameter::class), Vector3(Vector3Parameter::class), Vector4(Vector4Parameter::class), - Option(OptionParameter::class) + Option(OptionParameter::class), + Path(PathParameter::class) ; companion object { @@ -209,7 +233,12 @@ class Parameter( val invertY: Boolean?, val showVector: Boolean?, // val optionEnum: Enum<*>, - val order: Int) + val absolutePath: Boolean?, + val pathContext: String?, + val pathExtensions: Array?, + val pathIsDirectory: Boolean?, + val order: Int, + ) // // /** diff --git a/orx-parameters/src/jvmMain/kotlin/Annotations.kt b/orx-parameters/src/jvmMain/kotlin/Annotations.kt index d39e8832..b3f48c3d 100644 --- a/orx-parameters/src/jvmMain/kotlin/Annotations.kt +++ b/orx-parameters/src/jvmMain/kotlin/Annotations.kt @@ -31,6 +31,10 @@ fun Any.listParameters(): List { var vectorRange = Pair(Vector2(-1.0, -1.0), Vector2(1.0, 1.0)) var invertY: Boolean? = null var showVector: Boolean? = null + var absolutePath: Boolean? = null + var pathContext: String? = null + var pathExtensions: Array? = null + var pathIsDirectory: Boolean? = null for (it in annotations) { type = ParameterType.forParameterAnnotationClass(it) @@ -95,6 +99,13 @@ fun Any.listParameters(): List { label = it.label order = it.order } + is PathParameter -> { + label = it.label + absolutePath = it.absolute + pathContext = it.context + pathExtensions = it.extensions + pathIsDirectory = it.directory + } } } Parameter( @@ -109,7 +120,12 @@ fun Any.listParameters(): List { precision = precision, showVector = showVector, invertY = invertY, - order = order + absolutePath = absolutePath, + pathContext = pathContext, + pathExtensions = pathExtensions, + pathIsDirectory = pathIsDirectory, + order = order, + ) } + this::class.declaredMemberFunctions.filter { it.findAnnotation() != null @@ -130,6 +146,10 @@ fun Any.listParameters(): List { precision = null, showVector = null, invertY = null, + absolutePath = null, + pathContext = null, + pathExtensions = null, + pathIsDirectory = null, order = order ) }).sortedBy { it.order } diff --git a/orx-parameters/src/jvmTest/kotlin/TestAnnotations.kt b/orx-parameters/src/jvmTest/kotlin/TestAnnotations.kt index 1bda4cf8..891e505e 100644 --- a/orx-parameters/src/jvmTest/kotlin/TestAnnotations.kt +++ b/orx-parameters/src/jvmTest/kotlin/TestAnnotations.kt @@ -1,4 +1,5 @@ import org.amshove.kluent.* +import org.junit.jupiter.api.Assertions.assertEquals import org.openrndr.color.ColorRGBa import org.openrndr.extra.parameters.* import org.openrndr.math.Vector2 @@ -46,6 +47,9 @@ val a = object { @OptionParameter("an option parameter", order = 11) var o = ParameterType.Option + @PathParameter("a path parameter", order = 12) + var p = "bla.png" + } object TestAnnotations : Spek({ @@ -124,6 +128,12 @@ object TestAnnotations : Spek({ list[11].property?.name `should be equal to` "o" list[11].label `should be equal to` "an option parameter" + assertEquals(list[12].parameterType, ParameterType.Path) + assertEquals(list[12].property?.name, "p") + assertEquals(list[12].label, "a path parameter") + assertEquals(list[12].absolutePath, false) + assertEquals(list[12].pathContext, "null") + } } }) diff --git a/orx-property-watchers/README.md b/orx-property-watchers/README.md new file mode 100644 index 00000000..cede3fdd --- /dev/null +++ b/orx-property-watchers/README.md @@ -0,0 +1,3 @@ +# orx-property-watchers + +Tools for setting up property watcher based pipelines diff --git a/orx-property-watchers/build.gradle.kts b/orx-property-watchers/build.gradle.kts new file mode 100644 index 00000000..8c700d19 --- /dev/null +++ b/orx-property-watchers/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + org.openrndr.extra.convention.`kotlin-multiplatform` +} + +kotlin { + jvm { + testRuns["test"].executionTask { + useJUnitPlatform { + includeEngines("spek2") + } + } + } + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + implementation(libs.openrndr.application) + implementation(libs.openrndr.math) + implementation(libs.kotlin.reflect) + } + } + + + @Suppress("UNUSED_VARIABLE") + val jvmTest by getting { + dependencies { + implementation(libs.kluent) + implementation(libs.spek.dsl) + runtimeOnly(libs.spek.junit5) + runtimeOnly(libs.kotlin.reflect) + } + } + } +} \ No newline at end of file diff --git a/orx-property-watchers/src/commonMain/kotlin/PropertyWatcherDelegates.kt b/orx-property-watchers/src/commonMain/kotlin/PropertyWatcherDelegates.kt new file mode 100644 index 00000000..ac1c845c --- /dev/null +++ b/orx-property-watchers/src/commonMain/kotlin/PropertyWatcherDelegates.kt @@ -0,0 +1,136 @@ +package org.openrndr.extra.propertywatchers + +import org.openrndr.events.Event +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 + +/** + * Property watcher delegate + * @see watchingProperty + */ +class PropertyWatcherDelegate( + private val property: KProperty0, + private val valueChangedEvent: Event, + private val cleaner: ((R) -> Unit)? = null, + val function: (V) -> R +) { + private var watchValue: V? = null + private var value: R? = null + operator fun getValue(any: Any, property: KProperty<*>): R { + val ref = this.property.get() + if (watchValue != ref) { + watchValue = ref + value?.let { + cleaner?.invoke(it) + } + value = function(ref) + } + return value ?: error("no value?") + } +} + +/** + * Property watcher delegate + * @see watchingProperties + */ +class PropertyWatcherDelegate2( + private val toWatch0: KProperty0, + private val toWatch1: KProperty0, + private val cleaner: ((R) -> Unit)? = null, + private val function: (V0, V1) -> R +) { + private var watchValue0: V0? = null + private var watchValue1: V1? = null + private var value: R? = null + operator fun getValue(any: Any, property: KProperty<*>): R { + val ref0 = toWatch0.get() + val ref1 = toWatch1.get() + if (watchValue0 != ref0 || watchValue1 != ref1) { + watchValue0 = ref0 + watchValue1 = ref1 + + value?.let { + cleaner?.invoke(it) + } + value = function(ref0, ref1) + } + return value ?: error("no value?") + } +} + +/** + * Property watcher delegate + * @see watchingProperties + */ +class PropertyWatcherDelegate3( + private val toWatch0: KProperty0, + private val toWatch1: KProperty0, + private val toWatch2: KProperty0, + private val cleaner: ((R) -> Unit)? = null, + private val function: (V0, V1, V2) -> R +) { + private var watchValue0: V0? = null + private var watchValue1: V1? = null + private var watchValue2: V2? = null + private var value: R? = null + operator fun getValue(any: Any, property: KProperty<*>): R { + val ref0 = toWatch0.get() + val ref1 = toWatch1.get() + val ref2 = toWatch2.get() + if (watchValue0 != ref0 || watchValue1 != ref1 || watchValue2 != ref2) { + watchValue0 = ref0 + watchValue1 = ref1 + value?.let { + cleaner?.invoke(it) + } + value = function(ref0, ref1, ref2) + } + return value ?: error("no value?") + } +} + + +/** + * Delegate property value to a function for which the value of a single property is watched + * @param property the property for which to watch for value changes + * @param function a function that maps the property value to a new value + */ +fun watchingProperty( + property: KProperty0, + cleaner: ((R) -> Unit)? = null, + function: (value: V) -> R +): PropertyWatcherDelegate { + return PropertyWatcherDelegate(property, Event("value-changed-${property.name}"), cleaner, function) +} + +/** + * Delegate property value to a function for which the values of 2 properties are watched + * @param property0 the first property for which to watch for value changes + * @param property1 the second property which to watch for value changes + * @param function a function that maps the two property values to a new value + */ +fun watchingProperties( + property0: KProperty0, + property1: KProperty0, + cleaner: ((R) -> Unit)?, + function: (value0: V0, value1: V1) -> R +): PropertyWatcherDelegate2 { + return PropertyWatcherDelegate2(property0, property1, cleaner, function) +} + +/** + * Delegate property value to a function for which the values of 3 properties are watched + * @param property0 the first property for which to watch for value changes + * @param property1 the second property which to watch for value changes + * @param property2 the third property which to watch for value changes + * @param function a function that maps the three property values to a new value + */ +fun watchingProperties( + property0: KProperty0, + property1: KProperty0, + property2: KProperty0, + cleaner: ((R) -> Unit)? = null, + function: (value0: V0, value1: V1, value2: V2) -> R +): PropertyWatcherDelegate3 { + return PropertyWatcherDelegate3(property0, property1, property2, cleaner, function) +} \ No newline at end of file diff --git a/orx-property-watchers/src/jvmDemo/kotlin/DemoImagePathWatcher01.kt b/orx-property-watchers/src/jvmDemo/kotlin/DemoImagePathWatcher01.kt new file mode 100644 index 00000000..3c789dc3 --- /dev/null +++ b/orx-property-watchers/src/jvmDemo/kotlin/DemoImagePathWatcher01.kt @@ -0,0 +1,33 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.grayscale +import org.openrndr.draw.tint +import org.openrndr.drawImage +import org.openrndr.extra.propertywatchers.watchingImagePath +import org.openrndr.extra.propertywatchers.watchingProperty + +fun main() { + application { + program { + val state = object { + var path = "demo-data/images/image-001.png" + val image by watchingImagePath(::path) { + drawImage(it.width, it.height) { + drawer.drawStyle.colorMatrix = grayscale() + drawer.image(it) + } + } + val redImage by watchingProperty(::image, cleaner = { it.destroy() }) { + drawImage(it.width, it.height) { + drawer.drawStyle.colorMatrix = tint(ColorRGBa.RED) + drawer.image(it) + } + } + } + + extend { + drawer.image(state.redImage) + } + } + } +} \ No newline at end of file diff --git a/orx-property-watchers/src/jvmDemo/kotlin/DemoPropertyWatchers01.kt b/orx-property-watchers/src/jvmDemo/kotlin/DemoPropertyWatchers01.kt new file mode 100644 index 00000000..184ca994 --- /dev/null +++ b/orx-property-watchers/src/jvmDemo/kotlin/DemoPropertyWatchers01.kt @@ -0,0 +1,22 @@ +import org.openrndr.application +import org.openrndr.extra.propertywatchers.watchingProperty + +fun main() { + application { + program { + val state = object { + val x by watchingProperty(mouse::position) { + it.x + } + + val xx by watchingProperty(::x) { + it * it + } + } + + extend { + state.x + } + } + } +} \ No newline at end of file diff --git a/orx-property-watchers/src/jvmMain/kotlin/ImagePath.kt b/orx-property-watchers/src/jvmMain/kotlin/ImagePath.kt new file mode 100644 index 00000000..a189ccb8 --- /dev/null +++ b/orx-property-watchers/src/jvmMain/kotlin/ImagePath.kt @@ -0,0 +1,20 @@ +package org.openrndr.extra.propertywatchers + +import org.openrndr.Program +import org.openrndr.draw.ColorBuffer +import org.openrndr.draw.loadImage +import java.io.File +import kotlin.reflect.KProperty0 + +fun Program.watchingImagePath(pathProperty: KProperty0, imageTransform: (ColorBuffer) -> ColorBuffer = { it }) = + watchingProperty(pathProperty, cleaner = { it.destroy() }) { + val file = File(it) + require(file.exists()) { "$it does not exist" } + require(file.isFile) { "$it is not a file" } + val image = loadImage(file) + val transformedImage = imageTransform(image) + if (image != transformedImage) { + image.destroy() + } + transformedImage + } diff --git a/settings.gradle.kts b/settings.gradle.kts index e6e4d2b7..3b7728bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,6 +51,7 @@ include( "orx-jvm:orx-olive", "orx-jvm:orx-osc", "orx-palette", + "orx-property-watchers", "orx-jvm:orx-panel", "orx-jvm:orx-poisson-fill", "orx-quadtree",