[orx-parameters, orx-property-watchers, orx-file-watcher, orx-gui] Add @PathParameter, file watcher delegates and property delegates
This commit is contained in:
@@ -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 <T> 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> (() -> T).stop() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
watchers[this as () -> Any]?.stop()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers reload
|
||||
*/
|
||||
fun <T> (() -> T).triggerChange() {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
watchers[this as () -> Any]?.triggerChange()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* add watcher to file watcher
|
||||
*/
|
||||
fun <T, R> (() -> 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 <T> Program.watchFile(file: File, transducer: (File) -> T): () -> T = watchFile(this, file, transducer)
|
||||
|
||||
private val watching = mutableMapOf<Path, MutableList<FileWatcher>>()
|
||||
private val pathKeys = mutableMapOf<Path, WatchKey>()
|
||||
private val keyPaths = mutableMapOf<WatchKey, Path>()
|
||||
private val keyPaths = WeakHashMap<WatchKey, Path>()
|
||||
private val waiting = mutableMapOf<Path, Job>()
|
||||
|
||||
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<File>,
|
||||
requestStopEvent: Event<Unit>? = 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 <T> watchFile(
|
||||
file: File,
|
||||
contentsChangedEvent: Event<T>? = null,
|
||||
requestStopEvent: Event<Unit>? = null,
|
||||
transducer: (File) -> T
|
||||
): () -> T {
|
||||
var result = transducer(file)
|
||||
val fileChangedEvent = Event<File>()
|
||||
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 <T> Program.watchFile(file: File, onChange: Event<T>? = null, transducer: (File) -> T): () -> T =
|
||||
// watchFile(this, file, onChange, transducer = transducer)
|
||||
|
||||
@@ -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<T>(
|
||||
program: Program,
|
||||
file: File,
|
||||
valueChangedEvent: Event<T>? = null,
|
||||
requestStopEvent: Event<Unit>? = 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 <R> Program.watchingFile(
|
||||
file: File,
|
||||
valueChangedEvent: Event<R>? = null,
|
||||
requestStopEvent: Event<Unit>? = null,
|
||||
transducer: (File) -> R
|
||||
) = FileWatcherDelegate(this, file, valueChangedEvent, requestStopEvent, transducer)
|
||||
@@ -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)
|
||||
|
||||
28
orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt
Normal file
28
orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 <T : Any> getPersistedOrDefault(
|
||||
compartmentLabel: String,
|
||||
property: KMutableProperty1<Any, T>,
|
||||
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<StyleSheet> = 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<Any, String>,
|
||||
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<Any, String>,
|
||||
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<Map<String, Map<String, ParameterValue>>>() {}
|
||||
val labeledValues: Map<String, Map<String, ParameterValue>> = Gson().fromJson(json, typeToken.type)
|
||||
val labeledValues: Map<String, Map<String, ParameterValue>> = 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
|
||||
}
|
||||
|
||||
@@ -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 <T : Extension> 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 <T : Extension> 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<P : Program>(val resources: Resources? = null, private var scriptMod
|
||||
* reloads the active script
|
||||
*/
|
||||
fun reload() {
|
||||
watcher?.triggerChange()
|
||||
// watcher?.triggerChange()
|
||||
}
|
||||
|
||||
class ScriptWatcher
|
||||
|
||||
|
||||
|
||||
private var watcherRequestStopEvent = Event<Unit>()
|
||||
private var watcher: (() -> Unit)? = null
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@@ -99,7 +102,12 @@ class Olive<P : Program>(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<P : Program>(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<P : Program>(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()
|
||||
|
||||
Reference in New Issue
Block a user