[orx-jvm] Move panel, gui, dnk3, keyframer, triangulation to orx-jvm

This commit is contained in:
Edwin Jakobs
2021-06-27 21:32:24 +02:00
parent 5814acef8f
commit 874d49779f
159 changed files with 22 additions and 21 deletions

200
orx-jvm/orx-gui/README.md Normal file
View 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
![DemoOptions01Kt](https://github.com/openrndr/orx/blob/media/orx-gui/images/DemoOptions01Kt.png
[DemoSimple01Kt](src/demo/kotlin/DemoSimple01Kt.kt
![DemoSimple01Kt](https://github.com/openrndr/orx/blob/media/orx-gui/images/DemoSimple01Kt.png
<!-- __demos__ -->
## Demos
### DemoHide01
[source code](src/demo/kotlin/DemoHide01.kt)
![DemoHide01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoHide01Kt.png)
### DemoOptions01
[source code](src/demo/kotlin/DemoOptions01.kt)
![DemoOptions01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoOptions01Kt.png)
### DemoPresets01
[source code](src/demo/kotlin/DemoPresets01.kt)
![DemoPresets01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoPresets01Kt.png)
### DemoSimple01
[source code](src/demo/kotlin/DemoSimple01.kt)
![DemoSimple01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoSimple01Kt.png)
### DemoXYParameter
[source code](src/demo/kotlin/DemoXYParameter.kt)
![DemoXYParameterKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-gui/images/DemoXYParameterKt.png)

View 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)
}

View 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)
}
)
}
}
}

View 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)
}
}
}
}

View 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()])
}
}
}
}
}
}

View 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)
}
)
}
}
}

View 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)
}
}
}

View 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
}