[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm
This commit is contained in:
200
orx-jvm/orx-gui/README.md
Normal file
200
orx-jvm/orx-gui/README.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# orx-gui
|
||||
|
||||
Automatic UI (sliders, buttons, etc.) generated from annotated classes and properties. Uses `orx-panel` and `orx-parameters`.
|
||||
|
||||
A quick-and-dirty user interface toolkit.
|
||||
|
||||
`orx-gui` uses class and property annotations to generate simple interfaces. The annotations used
|
||||
are provided by [`orx-parameters`](../orx-parameters/README.md) and most filters in [`orx-fx`](../orx-fx/README.md) have been annotated.
|
||||
|
||||
`orx-gui` is made with an [`orx-olive`](../orx-olive/README.md) workflow in mind but can be used in normal OPENRNDR programs
|
||||
just as well.
|
||||
|
||||
## Usage
|
||||
|
||||
Preparation: make sure `orx-gui` is in the `orxFeatures` of your project (if you working on a template based project)
|
||||
|
||||
The essence of `orx-gui` lies in the provided a `GUI` extension, which can be used in your program using the `extend {}` function.
|
||||
The `GUI` class has an `add()` function that allows any annotated object to be passed in.
|
||||
|
||||
The visibility of the side bar can be toggled by pressing the F11 key on your keyboard.
|
||||
|
||||
### UIs for parameter objects
|
||||
|
||||
A simple UI can be created by creating an annotated `object`.
|
||||
|
||||
```kotlin
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.*
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.Vector4
|
||||
|
||||
enum class Option {
|
||||
Option1,
|
||||
Option2,
|
||||
Option3
|
||||
}
|
||||
|
||||
|
||||
fun main() = application {
|
||||
program {
|
||||
// -- this @Description annotation is optional
|
||||
val parameters = @Description("parameters") object {
|
||||
@DoubleParameter("radius", 20.0, 200.0, precision = 2, order = 0)
|
||||
var radius = 50.0
|
||||
|
||||
@TextParameter("A string", order = 1)
|
||||
var s = "Hello"
|
||||
|
||||
@BooleanParameter("A bool", order = 2)
|
||||
var b = true
|
||||
|
||||
@IntParameter("An int", 0, 127, order = 3)
|
||||
var i = 64
|
||||
|
||||
@ColorParameter("A fill color", order = 4)
|
||||
var fill = ColorRGBa.PINK
|
||||
|
||||
@XYParameter("Position", minX = 0.0, maxX = 640.0,
|
||||
minY = 0.0, maxY = 480.0, order = 5)
|
||||
var pos = Vector2.ZERO
|
||||
|
||||
@Vector2Parameter("A Vector2", order = 6)
|
||||
var v2 = Vector2(200.0, 200.0)
|
||||
|
||||
@Vector3Parameter("A Vector3", order = 7)
|
||||
var v3 = Vector3(200.0, 200.0, 200.0)
|
||||
|
||||
@Vector4Parameter("A Vector4", order = 8)
|
||||
var v4 = Vector4(200.0, 200.0, 200.0, 200.0)
|
||||
|
||||
@DoubleListParameter("Mixer", order = 9)
|
||||
var mixer = MutableList(5) { 0.5 }
|
||||
|
||||
@ActionParameter("Action test", order = 10)
|
||||
fun clicked() {
|
||||
println("GUI says hi!")
|
||||
}
|
||||
|
||||
@OptionParameter("An option", order = 11)
|
||||
var option = Option.Option1
|
||||
}
|
||||
|
||||
extend(GUI()) {
|
||||
add(parameters)
|
||||
}
|
||||
extend {
|
||||
drawer.fill = parameters.fill
|
||||
drawer.circle(parameters.pos, parameters.radius)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UIs for filters
|
||||
|
||||
In a similar fashion to the previous example we can create a simple UI for most filters in `orx-fx`
|
||||
|
||||
```kotlin
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.isolatedWithTarget
|
||||
import org.openrndr.draw.renderTarget
|
||||
import org.openrndr.extra.fx.blur.BoxBlur
|
||||
import org.openrndr.extra.gui.GUI
|
||||
|
||||
fun main() = application {
|
||||
program {
|
||||
val blur = BoxBlur()
|
||||
val rt = renderTarget(width, height) {
|
||||
colorBuffer()
|
||||
}
|
||||
extend(GUI()) {
|
||||
add(blur)
|
||||
}
|
||||
extend {
|
||||
drawer.isolatedWithTarget(rt) {
|
||||
drawer.background(ColorRGBa.BLACK)
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
drawer.circle(width / 2.0, height / 2.0, 200.0)
|
||||
}
|
||||
blur.apply(rt.colorBuffer(0), rt.colorBuffer(0))
|
||||
drawer.image(rt.colorBuffer(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### UIs in Olive
|
||||
|
||||
Using `orx-gui` in Olive (`orx-olive`) is very similar to how one would use it in a normal OPENRNDR program. There is
|
||||
one detail that doesn't occur in normal programs: the UI state is reset when a
|
||||
script is changed and re-evaluated. This is overcome by using an annotated `Reloadable` object.
|
||||
|
||||
An example `live.kts` script that uses `orx-gui` and `Reloadable`:
|
||||
```kotlin
|
||||
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.olive.Reloadable
|
||||
import org.openrndr.extra.parameters.DoubleParameter
|
||||
|
||||
{ program: Program ->
|
||||
program.apply {
|
||||
val p = object : Reloadable() {
|
||||
@DoubleParameter("x-position", 0.0, 640.0, order = 0)
|
||||
var x = 0.5
|
||||
@DoubleParameter("y-position", 0.0, 480.0, order = 1)
|
||||
var y = 0.5
|
||||
@DoubleParameter("radius", 0.0, 480.0, order = 2)
|
||||
var radius = 100.0
|
||||
}
|
||||
p.reload()
|
||||
|
||||
extend(GUI()) {
|
||||
add(p)
|
||||
}
|
||||
extend {
|
||||
drawer.circle(p.x, p.y, p.radius)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
`orx-gui` is based on a proof-of-concept by [Ricardo Matias](https://github.com/ricardomatias/)
|
||||
<!-- __demos__ >
|
||||
# Demos
|
||||
[DemoOptions01Kt](src/demo/kotlin/DemoOptions01Kt.kt
|
||||

|
||||
|
||||

|
||||
|
||||
### DemoOptions01
|
||||
[source code](src/demo/kotlin/DemoOptions01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoPresets01
|
||||
[source code](src/demo/kotlin/DemoPresets01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoSimple01
|
||||
[source code](src/demo/kotlin/DemoSimple01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoXYParameter
|
||||
[source code](src/demo/kotlin/DemoXYParameter.kt)
|
||||
|
||||

|
||||
22
orx-jvm/orx-gui/build.gradle
Normal file
22
orx-jvm/orx-gui/build.gradle
Normal file
@@ -0,0 +1,22 @@
|
||||
sourceSets {
|
||||
demo {
|
||||
java {
|
||||
srcDirs = ["src/demo/kotlin"]
|
||||
compileClasspath += main.getCompileClasspath()
|
||||
runtimeClasspath += main.getRuntimeClasspath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(":orx-parameters")
|
||||
api project(":orx-jvm:orx-panel")
|
||||
implementation "org.openrndr:openrndr-dialogs:$openrndrVersion"
|
||||
implementation "com.google.code.gson:gson:$gsonVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
|
||||
demoImplementation(sourceSets.getByName("main").output)
|
||||
}
|
||||
57
orx-jvm/orx-gui/src/demo/kotlin/DemoHide01.kt
Normal file
57
orx-jvm/orx-gui/src/demo/kotlin/DemoHide01.kt
Normal file
@@ -0,0 +1,57 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.*
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
|
||||
/**
|
||||
* A simple demonstration of a GUI for drawing some circles
|
||||
*/
|
||||
suspend 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
|
||||
|
||||
|
||||
val settings = @Description("Settings") object {
|
||||
@DoubleParameter("radius", 0.0, 100.0)
|
||||
var radius = 50.0
|
||||
|
||||
@Vector2Parameter("position", 0.0, 1.0)
|
||||
var position = Vector2(0.6, 0.5)
|
||||
|
||||
@ColorParameter("color")
|
||||
var color = ColorRGBa.PINK
|
||||
|
||||
@DoubleListParameter("radii", 5.0, 30.0)
|
||||
var radii = mutableListOf(5.0, 6.0, 8.0, 14.0, 20.0, 30.0)
|
||||
}
|
||||
gui.add(settings)
|
||||
extend(gui)
|
||||
|
||||
// note we can only change the visibility after the extend
|
||||
gui.visible = false
|
||||
|
||||
extend {
|
||||
// determine visibility through mouse x-coordinate
|
||||
gui.visible = mouse.position.x < 200.0
|
||||
|
||||
drawer.fill = settings.color
|
||||
drawer.circle(settings.position * drawer.bounds.position(1.0, 1.0), settings.radius)
|
||||
drawer.circles(
|
||||
settings.radii.mapIndexed { i, radius ->
|
||||
Circle(width - 50.0, 60.0 + i * 70.0, radius)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
orx-jvm/orx-gui/src/demo/kotlin/DemoOptions01.kt
Normal file
43
orx-jvm/orx-gui/src/demo/kotlin/DemoOptions01.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.*
|
||||
|
||||
/**
|
||||
* A simple demonstration of a GUI with a drop down menu
|
||||
*/
|
||||
|
||||
enum class BackgroundColors {
|
||||
Pink,
|
||||
Black,
|
||||
Yellow
|
||||
}
|
||||
|
||||
suspend 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
|
||||
val settings = @Description("Settings") object {
|
||||
@OptionParameter("Background color")
|
||||
var option = BackgroundColors.Pink
|
||||
}
|
||||
|
||||
gui.add(settings)
|
||||
extend(gui)
|
||||
extend {
|
||||
when(settings.option) {
|
||||
BackgroundColors.Pink -> drawer.clear(ColorRGBa.PINK)
|
||||
BackgroundColors.Black -> drawer.clear(ColorRGBa.BLACK)
|
||||
BackgroundColors.Yellow -> drawer.clear(ColorRGBa.YELLOW)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
orx-jvm/orx-gui/src/demo/kotlin/DemoPresets01.kt
Normal file
72
orx-jvm/orx-gui/src/demo/kotlin/DemoPresets01.kt
Normal file
@@ -0,0 +1,72 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.color.mix
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.*
|
||||
|
||||
/**
|
||||
* Shows how to store and retrieve in-memory gui presets.
|
||||
* Keyboard controls:
|
||||
* [Left Shift] + [0]..[9] => store current gui values to a preset
|
||||
* [0]..[9] => recall a preset
|
||||
*/
|
||||
suspend 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
|
||||
|
||||
val presets = MutableList(10) {
|
||||
gui.toObject()
|
||||
}
|
||||
|
||||
val settings = @Description("Settings") object {
|
||||
@IntParameter("a", 1, 10)
|
||||
var a = 7
|
||||
|
||||
@IntParameter("b", 1, 10)
|
||||
var b = 3
|
||||
|
||||
@ColorParameter("foreground")
|
||||
var foreground = ColorRGBa.fromHex("654062")
|
||||
|
||||
@ColorParameter("background")
|
||||
var background = ColorRGBa.fromHex("ff9c71")
|
||||
}
|
||||
gui.add(settings)
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.clear(settings.background)
|
||||
drawer.stroke = settings.background
|
||||
drawer.fill = settings.foreground
|
||||
// Draw a pattern based on modulo
|
||||
for(i in 0 until 100) {
|
||||
if(i % settings.a == 0 || i % settings.b == 0) {
|
||||
val x = (i % 10) * 64.0
|
||||
val y = (i / 10) * 48.0
|
||||
drawer.rectangle(x, y, 64.0, 48.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
keyboard.keyDown.listen {
|
||||
when (it.name) {
|
||||
in "0" .. "9" -> {
|
||||
if(keyboard.pressedKeys.contains("left-shift")) {
|
||||
// 1. Get the current gui state, store it in a list
|
||||
presets[it.name.toInt()] = gui.toObject()
|
||||
} else {
|
||||
// 2. Set the gui state
|
||||
gui.fromObject(presets[it.name.toInt()])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt
Normal file
49
orx-jvm/orx-gui/src/demo/kotlin/DemoSimple01.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.*
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.shape.Circle
|
||||
|
||||
/**
|
||||
* A simple demonstration of a GUI for drawing some circles
|
||||
*/
|
||||
suspend 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
|
||||
|
||||
val settings = @Description("Settings") object {
|
||||
@DoubleParameter("radius", 0.0, 100.0)
|
||||
var radius = 50.0
|
||||
|
||||
@Vector2Parameter("position", 0.0, 1.0)
|
||||
var position = Vector2(0.6, 0.5)
|
||||
|
||||
@ColorParameter("color")
|
||||
var color = ColorRGBa.PINK
|
||||
|
||||
@DoubleListParameter("radii", 5.0, 30.0)
|
||||
var radii = mutableListOf(5.0, 6.0, 8.0, 14.0, 20.0, 30.0)
|
||||
}
|
||||
gui.add(settings)
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.fill = settings.color
|
||||
drawer.circle(settings.position * drawer.bounds.position(1.0, 1.0), settings.radius)
|
||||
drawer.circles(
|
||||
settings.radii.mapIndexed { i, radius ->
|
||||
Circle(width - 50.0, 60.0 + i * 70.0, radius)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
orx-jvm/orx-gui/src/demo/kotlin/DemoXYParameter.kt
Normal file
40
orx-jvm/orx-gui/src/demo/kotlin/DemoXYParameter.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.gui.GUI
|
||||
import org.openrndr.extra.parameters.Description
|
||||
import org.openrndr.extra.parameters.XYParameter
|
||||
import org.openrndr.math.Vector2
|
||||
|
||||
suspend fun main() = application {
|
||||
configure {
|
||||
width = 800
|
||||
height = 800
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val settings = @Description("Settings") object {
|
||||
@XYParameter("Position", 0.0, 800.0, 0.0, 800.0,
|
||||
precision = 2,
|
||||
invertY = true,
|
||||
showVector = true)
|
||||
var position: Vector2 = Vector2(0.0,0.0)
|
||||
}
|
||||
|
||||
gui.add(settings)
|
||||
|
||||
extend(gui)
|
||||
extend {
|
||||
drawer.circle(settings.position, 50.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
900
orx-jvm/orx-gui/src/main/kotlin/Gui.kt
Normal file
900
orx-jvm/orx-gui/src/main/kotlin/Gui.kt
Normal file
@@ -0,0 +1,900 @@
|
||||
package org.openrndr.extra.gui
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
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.draw.Drawer
|
||||
import org.openrndr.extra.parameters.*
|
||||
import org.openrndr.internal.Driver
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.Vector4
|
||||
import org.openrndr.panel.ControlManager
|
||||
import org.openrndr.panel.controlManager
|
||||
import org.openrndr.panel.elements.*
|
||||
import org.openrndr.panel.style.*
|
||||
import java.io.File
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
|
||||
/** Dear contributor, just in case you are here looking to add a new parameter type.
|
||||
There is a 6-step incantation to add a new parameter type
|
||||
0) Add your parameter type to orx-parameters, follow the instructions provided there.
|
||||
|
||||
1) Setup a control style, very likely analogous to the styles already in place.
|
||||
2) Add control creation code.
|
||||
3) Add value serialization code, may need to update ParameterValue too.
|
||||
4) Add value deserialization code.
|
||||
5) Add value randomization code.
|
||||
6) Add control update code.
|
||||
|
||||
You can use your editor's search functionality to jump to "1)", "2)".
|
||||
*/
|
||||
private data class LabeledObject(val label: String, val obj: Any)
|
||||
|
||||
private class CompartmentState(var collapsed: Boolean, val parameterValues: MutableMap<String, Any> = mutableMapOf())
|
||||
private class SidebarState(var hidden: Boolean = false, var collapsed: Boolean = false, var scrollTop: Double = 0.0)
|
||||
private class TrackedObjectBinding(
|
||||
val parameters: List<Parameter>,
|
||||
val parameterControls: MutableMap<Parameter, Element> = mutableMapOf()
|
||||
)
|
||||
|
||||
private val persistentCompartmentStates = mutableMapOf<Long, MutableMap<String, CompartmentState>>()
|
||||
private val persistentSidebarStates = mutableMapOf<Long, SidebarState>()
|
||||
|
||||
private fun sidebarState(): SidebarState = persistentSidebarStates.getOrPut(Driver.instance.contextID) {
|
||||
SidebarState()
|
||||
}
|
||||
|
||||
private fun <T : Any> getPersistedOrDefault(compartmentLabel: String, property: KMutableProperty1<Any, T>, obj: Any): T? {
|
||||
val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel]
|
||||
if (state == null) {
|
||||
return property.get(obj)
|
||||
} else {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return (state.parameterValues[property.name] as? T?) ?: return property.get(obj)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> setAndPersist(compartmentLabel: String, property: KMutableProperty1<Any, T>, obj: Any, value: T) {
|
||||
property.set(obj, value)
|
||||
val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel]!!
|
||||
state.parameterValues[property.name] = value
|
||||
}
|
||||
|
||||
@Suppress("unused", "UNCHECKED_CAST")
|
||||
class GUI : Extension {
|
||||
private var onChangeListener: ((name: String, value: Any?) -> Unit)? = null
|
||||
override var enabled = true
|
||||
|
||||
var visible = true
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (field) {
|
||||
panel?.body?.classes?.remove(collapsed)
|
||||
} else {
|
||||
panel?.body?.classes?.add(collapsed)
|
||||
}
|
||||
sidebarState().hidden = !field
|
||||
}
|
||||
}
|
||||
|
||||
var compartmentsCollapsedByDefault = true
|
||||
var doubleBind = false
|
||||
|
||||
private var panel: ControlManager? = null
|
||||
|
||||
// Randomize button
|
||||
private var shiftDown = false
|
||||
private var randomizeButton: Button? = null // FIXME should this be null or is there a better way?
|
||||
|
||||
fun onChange(listener: (name: String, value: Any?) -> Unit) {
|
||||
onChangeListener = listener
|
||||
}
|
||||
|
||||
val collapsed = ElementClass("collapsed")
|
||||
|
||||
override fun setup(program: Program) {
|
||||
program.keyboard.keyDown.listen {
|
||||
if (it.key == KEY_F11) {
|
||||
println("f11 pressed")
|
||||
visible = !visible
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (it.key == KEY_LEFT_SHIFT) {
|
||||
shiftDown = true
|
||||
randomizeButton!!.classes.add(ElementClass("randomize-strong"))
|
||||
}
|
||||
}
|
||||
|
||||
program.keyboard.keyUp.listen {
|
||||
if (it.key == KEY_LEFT_SHIFT) {
|
||||
shiftDown = false
|
||||
randomizeButton!!.classes.remove(ElementClass("randomize-strong"))
|
||||
}
|
||||
}
|
||||
|
||||
panel = program.controlManager {
|
||||
styleSheet(has class_ "container") {
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Column
|
||||
this.width = 200.px
|
||||
this.height = 100.percent
|
||||
}
|
||||
|
||||
styleSheet(has class_ "collapse-border") {
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Column
|
||||
this.height = 5.px
|
||||
this.width = 100.percent
|
||||
this.background = Color.RGBa(ColorRGBa.GRAY.shade(0.9))
|
||||
|
||||
and(has state "hover") {
|
||||
this.background = Color.RGBa(ColorRGBa.GRAY.shade(1.1))
|
||||
}
|
||||
}
|
||||
|
||||
styleSheet(has class_ "toolbar") {
|
||||
this.height = 42.px
|
||||
this.width = 100.percent
|
||||
this.display = Display.FLEX
|
||||
this.flexDirection = FlexDirection.Row
|
||||
this.background = Color.RGBa(ColorRGBa.GRAY.copy(a = 0.99))
|
||||
}
|
||||
|
||||
styleSheet(has class_ "collapsed") {
|
||||
this.display = Display.NONE
|
||||
}
|
||||
|
||||
styleSheet(has class_ "compartment") {
|
||||
this.paddingBottom = 20.px
|
||||
}
|
||||
|
||||
styleSheet(has class_ "sidebar") {
|
||||
this.width = 200.px
|
||||
this.paddingBottom = 20.px
|
||||
this.paddingTop = 10.px
|
||||
this.paddingLeft = 10.px
|
||||
this.paddingRight = 10.px
|
||||
this.marginRight = 2.px
|
||||
this.height = 100.percent
|
||||
this.background = Color.RGBa(ColorRGBa.GRAY.copy(a = 0.99))
|
||||
this.overflow = Overflow.Scroll
|
||||
|
||||
//<editor-fold desc="1) setup control style">
|
||||
descendant(has type "colorpicker-button") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
descendant(has type "slider") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
descendant(has type "button") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
descendant(has type "textfield") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
descendant(has type "toggle") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
descendant(has type "xy-pad") {
|
||||
this.width = 175.px
|
||||
this.height = 175.px
|
||||
}
|
||||
|
||||
descendant(has type listOf(
|
||||
"sequence-editor",
|
||||
"sliders-vector2",
|
||||
"sliders-vector3",
|
||||
"sliders-vector4"
|
||||
)) {
|
||||
this.width = 175.px
|
||||
this.height = 100.px
|
||||
}
|
||||
//</editor-fold>
|
||||
}
|
||||
|
||||
styleSheet(has class_ "randomize-strong") {
|
||||
color = Color.RGBa(ColorRGBa.PINK)
|
||||
|
||||
and(has state "hover") {
|
||||
color = Color.RGBa(ColorRGBa.BLACK)
|
||||
background = Color.RGBa(ColorRGBa.PINK)
|
||||
}
|
||||
}
|
||||
|
||||
styleSheet(has type "dropdown-button") {
|
||||
this.width = 175.px
|
||||
}
|
||||
|
||||
|
||||
layout {
|
||||
div("container") {
|
||||
id = "container"
|
||||
@Suppress("UNUSED_VARIABLE") val header = div("toolbar") {
|
||||
randomizeButton = button {
|
||||
label = "Randomize"
|
||||
clicked {
|
||||
randomize(strength = if (shiftDown) .75 else .05)
|
||||
}
|
||||
}
|
||||
button {
|
||||
label = "Load"
|
||||
clicked {
|
||||
openFileDialog(supportedExtensions = listOf("json"), contextID = "gui.parameters") {
|
||||
loadParameters(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
label = "Save"
|
||||
clicked {
|
||||
val defaultPath = getDefaultPathForContext(contextID = "gui.parameters")
|
||||
|
||||
if (defaultPath == null) {
|
||||
val local = File(".")
|
||||
val data = File(local, "data")
|
||||
if (data.exists() && data.isDirectory) {
|
||||
val parameters = File(data, "parameters")
|
||||
if (!parameters.exists()) {
|
||||
if (parameters.mkdirs()) {
|
||||
setDefaultPathForContext(contextID = "gui.parameters", file = parameters)
|
||||
}
|
||||
} else {
|
||||
setDefaultPathForContext(contextID = "gui.parameters", file = parameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveFileDialog(suggestedFilename = "parameters.json", contextID = "gui.parameters", supportedExtensions = listOf("json")) {
|
||||
saveParameters(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val collapseBorder = div("collapse-border") {
|
||||
|
||||
}
|
||||
|
||||
val collapsibles = mutableSetOf<Div>()
|
||||
val sidebar = div("sidebar") {
|
||||
id = "sidebar"
|
||||
scrollTop = sidebarState().scrollTop
|
||||
for ((labeledObject, binding) in trackedObjects) {
|
||||
val (label, _) = labeledObject
|
||||
|
||||
val h3Header = h3 { label }
|
||||
val collapsible = div("compartment") {
|
||||
for (parameter in binding.parameters) {
|
||||
val element = addControl(labeledObject, parameter)
|
||||
binding.parameterControls[parameter] = element
|
||||
}
|
||||
}
|
||||
collapsibles.add(collapsible)
|
||||
val collapseClass = ElementClass("collapsed")
|
||||
|
||||
/* this is guaranteed to be in the dictionary after insertion through add() */
|
||||
val collapseState = persistentCompartmentStates[Driver.instance.contextID]!![label]!!
|
||||
if (collapseState.collapsed) {
|
||||
collapsible.classes.add(collapseClass)
|
||||
}
|
||||
|
||||
h3Header.mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
h3Header.mouse.clicked.listen { me ->
|
||||
|
||||
if (KeyModifier.CTRL in me.modifiers) {
|
||||
collapsible.classes.remove(collapseClass)
|
||||
persistentCompartmentStates[Driver.instance.contextID]!!.forEach {
|
||||
it.value.collapsed = true
|
||||
}
|
||||
collapseState.collapsed = false
|
||||
|
||||
(collapsibles - collapsible).forEach {
|
||||
it.classes.add(collapseClass)
|
||||
}
|
||||
} else {
|
||||
|
||||
if (collapseClass in collapsible.classes) {
|
||||
collapsible.classes.remove(collapseClass)
|
||||
collapseState.collapsed = false
|
||||
} else {
|
||||
collapsible.classes.add(collapseClass)
|
||||
collapseState.collapsed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collapseBorder.mouse.pressed.listen {
|
||||
it.cancelPropagation()
|
||||
}
|
||||
|
||||
collapseBorder.mouse.clicked.listen {
|
||||
val collapsed = ElementClass("collapsed")
|
||||
if (collapsed in sidebar.classes) {
|
||||
sidebar.classes.remove(collapsed)
|
||||
sidebarState().collapsed = false
|
||||
} else {
|
||||
sidebar.classes.add(collapsed)
|
||||
sidebarState().collapsed = true
|
||||
}
|
||||
it.cancelPropagation()
|
||||
}
|
||||
sidebar.mouse.scrolled.listen {
|
||||
sidebarState().scrollTop = sidebar.scrollTop
|
||||
}
|
||||
if (sidebarState().collapsed) {
|
||||
sidebar.classes.add(ElementClass("collapsed"))
|
||||
}
|
||||
sidebar.scrollTop = sidebarState().scrollTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visible = !sidebarState().hidden
|
||||
|
||||
|
||||
program.extend(panel ?: error("no panel"))
|
||||
}
|
||||
|
||||
/* 2) control creation. create control, set label, set range, setup event-handler, load values */
|
||||
//<editor-fold desc="2) Control creation">
|
||||
private fun Div.addControl(compartment: LabeledObject, parameter: Parameter): Element {
|
||||
val obj = compartment.obj
|
||||
|
||||
return when (parameter.parameterType) {
|
||||
|
||||
ParameterType.Int -> {
|
||||
slider {
|
||||
label = parameter.label
|
||||
range = Range(parameter.intRange!!.first.toDouble(), parameter.intRange!!.last.toDouble())
|
||||
precision = 0
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj, it.newValue.toInt())
|
||||
(parameter.property as KMutableProperty1<Any, Int>).set(obj, value.toInt())
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj)?.let {
|
||||
value = it.toDouble()
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Int>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Double -> {
|
||||
slider {
|
||||
label = parameter.label
|
||||
range = Range(parameter.doubleRange!!.start, parameter.doubleRange!!.endInclusive)
|
||||
precision = parameter.precision!!
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj, it.newValue)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj)?.let {
|
||||
value = it
|
||||
/* this is generally not needed, but when the persisted value is equal to the slider default
|
||||
it will not emit the newly set value */
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Double>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Action -> {
|
||||
button {
|
||||
label = parameter.label
|
||||
events.clicked.listen {
|
||||
/* the `obj` we pass in here is the receiver */
|
||||
parameter.function!!.call(obj)
|
||||
onChangeListener?.invoke(parameter.function!!.name, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Boolean -> {
|
||||
toggle {
|
||||
label = parameter.label
|
||||
events.valueChanged.listen {
|
||||
value = it.newValue
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj, it.newValue)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj)?.let {
|
||||
value = it
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Boolean>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Text -> {
|
||||
textfield {
|
||||
label = parameter.label
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, String>, obj, it.newValue)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(compartment.label, parameter.property as KMutableProperty1<Any, String>, obj)?.let {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Color -> {
|
||||
colorpickerButton {
|
||||
label = parameter.label
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, ColorRGBa>,
|
||||
obj,
|
||||
it.color
|
||||
)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.color)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, ColorRGBa>,
|
||||
obj
|
||||
)?.let {
|
||||
color = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.XY -> {
|
||||
xyPad {
|
||||
minX = parameter.vectorRange!!.first.x
|
||||
minY = parameter.vectorRange!!.first.y
|
||||
maxX = parameter.vectorRange!!.second.x
|
||||
maxY = parameter.vectorRange!!.second.y
|
||||
precision = parameter.precision!!
|
||||
showVector = parameter.showVector!!
|
||||
invertY = parameter.invertY!!
|
||||
label = parameter.label
|
||||
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector2>,
|
||||
obj,
|
||||
it.newValue
|
||||
)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.DoubleList -> {
|
||||
sequenceEditor {
|
||||
range = parameter.doubleRange!!
|
||||
label = parameter.label
|
||||
minimumSequenceLength = parameter.sizeRange!!.start
|
||||
maximumSequenceLength = parameter.sizeRange!!.endInclusive
|
||||
precision = parameter.precision!!
|
||||
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, MutableList<Double>>,
|
||||
obj,
|
||||
it.newValue.toMutableList()
|
||||
)
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, MutableList<Double>>,
|
||||
obj
|
||||
)?.let {
|
||||
value = it
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, MutableList<Double>>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.Vector2 -> {
|
||||
slidersVector2 {
|
||||
range = parameter.doubleRange!!
|
||||
label = parameter.label
|
||||
precision = parameter.precision!!
|
||||
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector2>,
|
||||
obj,
|
||||
it.newValue)
|
||||
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector2>,
|
||||
obj
|
||||
)?.let {
|
||||
value = it
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector2>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.Vector3 -> {
|
||||
slidersVector3 {
|
||||
range = parameter.doubleRange!!
|
||||
label = parameter.label
|
||||
precision = parameter.precision!!
|
||||
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector3>,
|
||||
obj,
|
||||
it.newValue)
|
||||
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector3>,
|
||||
obj
|
||||
)?.let {
|
||||
value = it
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector3>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ParameterType.Vector4 -> {
|
||||
slidersVector4 {
|
||||
range = parameter.doubleRange!!
|
||||
label = parameter.label
|
||||
precision = parameter.precision!!
|
||||
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector4>,
|
||||
obj,
|
||||
it.newValue)
|
||||
|
||||
onChangeListener?.invoke(parameter.property!!.name, it.newValue)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Vector4>,
|
||||
obj
|
||||
)?.let {
|
||||
value = it
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Vector4>, obj, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
ParameterType.Option -> {
|
||||
dropdownButton {
|
||||
val enumProperty = parameter.property as KMutableProperty1<Any, Enum<*>>
|
||||
val value = enumProperty.get(obj)
|
||||
label = parameter.label
|
||||
// -- this is dirty, but it is the only way to get the constants for arbitrary enums
|
||||
// -- (that I know of, at least)
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") val jEnum = value as java.lang.Enum<*>
|
||||
// -- we don't use the property syntax here because that leads to compilation errors
|
||||
@Suppress("UsePropertyAccessSyntax") val constants = jEnum.getDeclaringClass().getEnumConstants()
|
||||
constants.forEach {
|
||||
item {
|
||||
label = it.name
|
||||
data = it
|
||||
}
|
||||
}
|
||||
events.valueChanged.listen {
|
||||
setAndPersist(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Enum<*>>,
|
||||
obj,
|
||||
it.value.data as? Enum<*> ?: error("no data")
|
||||
)
|
||||
}
|
||||
getPersistedOrDefault(
|
||||
compartment.label,
|
||||
parameter.property as KMutableProperty1<Any, Enum<*>>,
|
||||
obj
|
||||
)?.let { enum ->
|
||||
(this@dropdownButton).value = items().find { item -> item.data == enum }
|
||||
?: error("no matching item found")
|
||||
setAndPersist(compartment.label, parameter.property as KMutableProperty1<Any, Enum<*>>, obj, enum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//</editor-fold>
|
||||
|
||||
private val trackedObjects = mutableMapOf<LabeledObject, TrackedObjectBinding>()
|
||||
|
||||
private fun updateControls() {
|
||||
for ((labeledObject, binding) in trackedObjects) {
|
||||
for ((parameter, control) in binding.parameterControls) {
|
||||
updateControl(labeledObject, parameter, control)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ParameterValue(var doubleValue: Double? = null,
|
||||
var intValue: Int? = null,
|
||||
var booleanValue: Boolean? = null,
|
||||
var colorValue: ColorRGBa? = null,
|
||||
var vector2Value: Vector2? = null,
|
||||
var vector3Value: Vector3? = null,
|
||||
var vector4Value: Vector4? = null,
|
||||
var doubleListValue: MutableList<Double>? = null,
|
||||
var textValue: String? = null,
|
||||
var optionValue: String? = null
|
||||
|
||||
)
|
||||
|
||||
/**
|
||||
* Can be called by the user to obtain an object to be serialized
|
||||
* externally. This allows the user to combine custom data with gui
|
||||
* state and save it all to one file. Complements `.fromObject()`.
|
||||
*/
|
||||
fun toObject(): Map<String, Map<String, ParameterValue>> {
|
||||
fun <T> KMutableProperty1<out Any, Any?>?.qget(obj: Any): T {
|
||||
return (this as KMutableProperty1<Any, T>).get(obj)
|
||||
}
|
||||
|
||||
return trackedObjects.entries.associate { (lo, b) ->
|
||||
Pair(lo.label, b.parameterControls.keys.associate { k ->
|
||||
Pair(k.property?.name ?: k.function?.name
|
||||
?: error("no name"), when (k.parameterType) {
|
||||
/* 3) setup serializers */
|
||||
ParameterType.Double -> ParameterValue(doubleValue = k.property.qget(lo.obj) as Double)
|
||||
ParameterType.Int -> ParameterValue(intValue = k.property.qget(lo.obj) as Int)
|
||||
ParameterType.Action -> ParameterValue()
|
||||
ParameterType.Color -> ParameterValue(colorValue = k.property.qget(lo.obj) as ColorRGBa)
|
||||
ParameterType.Text -> ParameterValue(textValue = k.property.qget(lo.obj) as String)
|
||||
ParameterType.Boolean -> ParameterValue(booleanValue = k.property.qget(lo.obj) as Boolean)
|
||||
ParameterType.XY -> ParameterValue(vector2Value = k.property.qget(lo.obj) as Vector2)
|
||||
ParameterType.DoubleList -> ParameterValue(doubleListValue = k.property.qget(lo.obj) as MutableList<Double>)
|
||||
ParameterType.Vector2 -> ParameterValue(vector2Value = k.property.qget(lo.obj) as Vector2)
|
||||
ParameterType.Vector3 -> ParameterValue(vector3Value = k.property.qget(lo.obj) as Vector3)
|
||||
ParameterType.Vector4 -> ParameterValue(vector4Value = k.property.qget(lo.obj) as Vector4)
|
||||
ParameterType.Option -> ParameterValue(optionValue = (k.property.qget(lo.obj) as Enum<*>).name)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun saveParameters(file: File) {
|
||||
file.writeText(Gson().toJson(toObject()))
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called by the user to update the gui using an object
|
||||
* deserialized externally. Allows the user to load a larger json object,
|
||||
* deserialize it, and use part of it to update the GUI.
|
||||
* Complements `.toObject()`.
|
||||
*/
|
||||
fun fromObject(labeledValues: Map<String, Map<String, ParameterValue>>) {
|
||||
fun <T> KMutableProperty1<out Any, Any?>?.qset(obj: Any, value: T) =
|
||||
(this as KMutableProperty1<Any, T>).set(obj, value)
|
||||
|
||||
fun KMutableProperty1<out Any, Any?>?.enumSet(obj: Any, value: String) {
|
||||
val v = (this as KMutableProperty1<Any, Enum<*>>).get(obj)
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UsePropertyAccessSyntax")
|
||||
val enumValue = (v as java.lang.Enum<*>).getDeclaringClass().getEnumConstants().find { it.name == value }
|
||||
?: error("cannot map value $value to enum")
|
||||
(this as KMutableProperty1<Any, Enum<*>>).set(obj, enumValue)
|
||||
}
|
||||
|
||||
labeledValues.forEach { (label, ps) ->
|
||||
trackedObjects.keys.find { it.label == label }?.let { lo ->
|
||||
val binding = trackedObjects[lo]!!
|
||||
ps.forEach { (parameterName, parameterValue) ->
|
||||
binding.parameters.find { it.property?.name == parameterName }?.let { parameter ->
|
||||
when (parameter.parameterType) {
|
||||
/* 4) Set up deserializers */
|
||||
ParameterType.Double -> parameterValue.doubleValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Int -> parameterValue.intValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Text -> parameterValue.textValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Color -> parameterValue.colorValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.XY -> parameterValue.vector2Value?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.DoubleList -> parameterValue.doubleListValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Boolean -> parameterValue.booleanValue?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Vector2 -> parameterValue.vector2Value?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Vector3 -> parameterValue.vector3Value?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Vector4 -> parameterValue.vector4Value?.let {
|
||||
parameter.property.qset(lo.obj, it)
|
||||
}
|
||||
ParameterType.Option -> parameterValue.optionValue?.let {
|
||||
parameter.property.enumSet(lo.obj, it)
|
||||
}
|
||||
ParameterType.Action -> {
|
||||
// intentionally do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updateControls()
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
fromObject(labeledValues)
|
||||
}
|
||||
|
||||
private fun updateControl(labeledObject: LabeledObject, parameter: Parameter, control: Element) {
|
||||
when (parameter.parameterType) {
|
||||
/* 5) Update control from property value */
|
||||
ParameterType.Double -> {
|
||||
(control as Slider).value = (parameter.property as KMutableProperty1<Any, Double>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Int -> {
|
||||
(control as Slider).value = (parameter.property as KMutableProperty1<Any, Int>).get(labeledObject.obj).toDouble()
|
||||
}
|
||||
ParameterType.Text -> {
|
||||
(control as Textfield).value = (parameter.property as KMutableProperty1<Any, String>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Color -> {
|
||||
(control as ColorpickerButton).color = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.XY -> {
|
||||
(control as XYPad).value = (parameter.property as KMutableProperty1<Any, Vector2>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.DoubleList -> {
|
||||
(control as SequenceEditor).value = (parameter.property as KMutableProperty1<Any, MutableList<Double>>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Boolean -> {
|
||||
(control as Toggle).value = (parameter.property as KMutableProperty1<Any, Boolean>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Vector2 -> {
|
||||
(control as SlidersVector2).value = (parameter.property as KMutableProperty1<Any, Vector2>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Vector3 -> {
|
||||
(control as SlidersVector3).value = (parameter.property as KMutableProperty1<Any, Vector3>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Vector4 -> {
|
||||
(control as SlidersVector4).value = (parameter.property as KMutableProperty1<Any, Vector4>).get(labeledObject.obj)
|
||||
}
|
||||
ParameterType.Option -> {
|
||||
val ddb = control as DropdownButton
|
||||
ddb.value = ddb.items().find { item -> item.data == (parameter.property as KMutableProperty1<Any, Enum<*>>).get(labeledObject.obj) } ?: error("could not find item")
|
||||
}
|
||||
ParameterType.Action -> {
|
||||
// intentionally do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun randomize(strength: Double = 0.05) {
|
||||
for ((labeledObject, binding) in trackedObjects) {
|
||||
// -- only randomize visible parameters
|
||||
for (parameter in binding.parameterControls.keys) {
|
||||
when (parameter.parameterType) {
|
||||
/* 6) Set up value randomizers */
|
||||
ParameterType.Double -> {
|
||||
val min = parameter.doubleRange!!.start
|
||||
val max = parameter.doubleRange!!.endInclusive
|
||||
val currentValue = (parameter.property as KMutableProperty1<Any, Double>).get(labeledObject.obj)
|
||||
val randomValue = Math.random() * (max - min) + min
|
||||
val newValue = (1.0 - strength) * currentValue + randomValue * strength
|
||||
(parameter.property as KMutableProperty1<Any, Double>).set(labeledObject.obj, newValue)
|
||||
}
|
||||
ParameterType.Int -> {
|
||||
val min = parameter.intRange!!.first
|
||||
val max = parameter.intRange!!.last
|
||||
val currentValue = (parameter.property as KMutableProperty1<Any, Int>).get(labeledObject.obj)
|
||||
val randomValue = Math.random() * (max - min) + min
|
||||
val newValue = ((1.0 - strength) * currentValue + randomValue * strength).roundToInt()
|
||||
(parameter.property as KMutableProperty1<Any, Int>).set(labeledObject.obj, newValue)
|
||||
}
|
||||
ParameterType.Boolean -> {
|
||||
//I am not sure about randomizing boolean values here
|
||||
//(parameter.property as KMutableProperty1<Any, Boolean>).set(labeledObject.obj, (Math.random() < 0.5))
|
||||
}
|
||||
ParameterType.Color -> {
|
||||
val currentValue = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(labeledObject.obj)
|
||||
val randomValue = ColorRGBa(Math.random(), Math.random(), Math.random(), currentValue.a)
|
||||
val newValue = ColorRGBa((1.0 - strength) * currentValue.r + randomValue.r * strength,
|
||||
(1.0 - strength) * currentValue.g + randomValue.g * strength,
|
||||
(1.0 - strength) * currentValue.b + randomValue.b * strength)
|
||||
|
||||
(parameter.property as KMutableProperty1<Any, ColorRGBa>).set(labeledObject.obj, newValue)
|
||||
}
|
||||
else -> {
|
||||
// intentionally do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updateControls()
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find a unique label
|
||||
* @param label to find an alternate for in case it already exist
|
||||
*/
|
||||
private fun resolveUniqueLabel(label: String): String {
|
||||
return trackedObjects.keys.find { it.label == label }?.let { lo ->
|
||||
resolveUniqueLabel(Regex("(.*) / ([0-9]+)").matchEntire(lo.label)?.let {
|
||||
"${it.groupValues[1]} / ${1 + it.groupValues[2].toInt()}"
|
||||
} ?: "$label / 2")
|
||||
} ?: label
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object to the GUI
|
||||
* @param objectWithParameters an object of a class that annotated parameters
|
||||
* @param label an optional label that overrides the label supplied in a [Description] annotation
|
||||
* @return pass-through of [objectWithParameters]
|
||||
*/
|
||||
fun <T : Any> add(objectWithParameters: T, label: String? = objectWithParameters.title()): T {
|
||||
val parameters = objectWithParameters.listParameters()
|
||||
val uniqueLabel = resolveUniqueLabel(label ?: "No name")
|
||||
|
||||
if (parameters.isNotEmpty()) {
|
||||
val collapseStates = persistentCompartmentStates.getOrPut(Driver.instance.contextID) {
|
||||
mutableMapOf()
|
||||
}
|
||||
collapseStates.getOrPut(uniqueLabel) {
|
||||
CompartmentState(compartmentsCollapsedByDefault)
|
||||
}
|
||||
trackedObjects[LabeledObject(uniqueLabel, objectWithParameters)] = TrackedObjectBinding(parameters)
|
||||
}
|
||||
return objectWithParameters
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object to the GUI using a builder.
|
||||
* @param label an optional label that overrides the label supplied in a [Description] annotation
|
||||
* @return the built object
|
||||
*/
|
||||
fun <T : Any> add(label: String? = null, builder: () -> T): T {
|
||||
val t = builder()
|
||||
return add(t, label ?: t.title())
|
||||
}
|
||||
|
||||
override fun afterDraw(drawer: Drawer, program: Program) {
|
||||
if (doubleBind) {
|
||||
updateControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("addToGui")
|
||||
fun <T : Any> T.addTo(gui: GUI, label: String? = this.title()): T {
|
||||
gui.add(this, label)
|
||||
return this
|
||||
}
|
||||
Reference in New Issue
Block a user