Rework and fix for orx-parameter/orx-gui ButtonParameter (now ActionParameter)

This commit is contained in:
Edwin Jakobs
2020-02-04 12:24:18 +01:00
parent 9ca639a14a
commit 2738b2f287
5 changed files with 106 additions and 46 deletions

View File

@@ -15,6 +15,8 @@ Preparation: make sure `orx-gui` is in the `orxFeatures` of your project (if you
The essence of `orx-gui` lies in the provided a `GUI` extension, which can be used in your program using the `extend {}` function. 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 `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 ### UIs for parameter objects
A simple UI can be created by creating an annotated `object`. A simple UI can be created by creating an annotated `object`.

View File

@@ -1,6 +1,7 @@
package org.openrndr.extra.gui package org.openrndr.extra.gui
import org.openrndr.Extension import org.openrndr.Extension
import org.openrndr.KEY_F11
import org.openrndr.Program import org.openrndr.Program
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.extra.parameters.* import org.openrndr.extra.parameters.*
@@ -22,6 +23,14 @@ class GUI : Extension {
} }
override fun setup(program: Program) { override fun setup(program: Program) {
program.keyboard.keyDown.listen {
if (it.key == KEY_F11) {
enabled = !enabled
panel.enabled = enabled
}
}
panel = program.controlManager { panel = program.controlManager {
styleSheet(has class_ "container") { styleSheet(has class_ "container") {
this.display = Display.FLEX this.display = Display.FLEX
@@ -80,39 +89,39 @@ class GUI : Extension {
} }
private fun Div.addControl(obj: Any, parameter: Parameter) { private fun Div.addControl(obj: Any, parameter: Parameter) {
when(parameter.parameterType) { when(parameter.parameterType) {
ParameterType.Int -> { ParameterType.Int -> {
slider { slider {
label = parameter.label label = parameter.label
range = Range(parameter.intRange!!.start.toDouble(), parameter.intRange!!.endInclusive.toDouble()) range = Range(parameter.intRange!!.first.toDouble(), parameter.intRange!!.last.toDouble())
precision = 0 precision = 0
value = (parameter.property as KMutableProperty1<Any, Int>).get(obj).toDouble() value = (parameter.property as KMutableProperty1<Any, Int>).get(obj).toDouble()
events.valueChanged.subscribe { events.valueChanged.subscribe {
(parameter.property as KMutableProperty1<Any, Int>).set(obj, value.toInt()) (parameter.property as KMutableProperty1<Any, Int>).set(obj, value.toInt())
onChangeListener?.invoke(parameter.property.name, it.newValue) onChangeListener?.invoke(parameter.property!!.name, it.newValue)
} }
} }
} }
ParameterType.Double -> { ParameterType.Double -> {
slider { slider {
label = parameter.label label = parameter.label
range = Range(parameter.doubleRange!!.start, parameter.doubleRange!!.endInclusive.toDouble()) range = Range(parameter.doubleRange!!.start, parameter.doubleRange!!.endInclusive)
precision = parameter.precision!! precision = parameter.precision!!
value = (parameter.property as KMutableProperty1<Any, Double>).get(obj) value = (parameter.property as KMutableProperty1<Any, Double>).get(obj)
events.valueChanged.subscribe { events.valueChanged.subscribe {
(parameter.property as KMutableProperty1<Any, Double>).set(obj, value) (parameter.property as KMutableProperty1<Any, Double>).set(obj, value)
onChangeListener?.invoke(parameter.property.name, it.newValue) onChangeListener?.invoke(parameter.property!!.name, it.newValue)
} }
} }
} }
ParameterType.Button -> { ParameterType.Action -> {
button { button {
label = parameter.label label = parameter.label
events.clicked.subscribe { events.clicked.subscribe {
parameter.property.call(obj) /* the `obj` we pass in here is the receiver */
onChangeListener?.invoke(parameter.property.name, null) parameter.function!!.call(obj)
onChangeListener?.invoke(parameter.function!!.name, null)
} }
} }
} }
@@ -126,7 +135,7 @@ class GUI : Extension {
events.valueChanged.subscribe { events.valueChanged.subscribe {
value = it.newValue value = it.newValue
(parameter.property as KMutableProperty1<Any, Boolean>).set(obj, value > 0.5) (parameter.property as KMutableProperty1<Any, Boolean>).set(obj, value > 0.5)
onChangeListener?.invoke(parameter.property.name, it.newValue) onChangeListener?.invoke(parameter.property!!.name, it.newValue)
} }
} }
} }
@@ -138,7 +147,7 @@ class GUI : Extension {
events.valueChanged.subscribe { events.valueChanged.subscribe {
value = it.newValue value = it.newValue
(parameter.property as KMutableProperty1<Any, String>).set(obj, value) (parameter.property as KMutableProperty1<Any, String>).set(obj, value)
onChangeListener?.invoke(parameter.property.name, it.newValue) onChangeListener?.invoke(parameter.property!!.name, it.newValue)
} }
} }
} }
@@ -149,21 +158,21 @@ class GUI : Extension {
color = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(obj) color = (parameter.property as KMutableProperty1<Any, ColorRGBa>).get(obj)
events.valueChanged.subscribe { events.valueChanged.subscribe {
(parameter.property as KMutableProperty1<Any, ColorRGBa>).set(obj, it.color) (parameter.property as KMutableProperty1<Any, ColorRGBa>).set(obj, it.color)
onChangeListener?.invoke(parameter.property.name, it.color) onChangeListener?.invoke(parameter.property!!.name, it.color)
} }
} }
} }
} }
} }
val trackedParams = mutableMapOf<Any, List<Parameter>>() private val trackedParams = mutableMapOf<Any, List<Parameter>>()
/** /**
* Add an object to the GUI * Add an object to the GUI
*/ */
fun add(objectWithParameters: Any) { fun add(objectWithParameters: Any) {
val parameters = objectWithParameters.listParameters() val parameters = objectWithParameters.listParameters()
if (parameters.size > 0) { if (parameters.isNotEmpty()) {
trackedParams[objectWithParameters] = parameters trackedParams[objectWithParameters] = parameters
} }
} }

View File

@@ -3,12 +3,17 @@
A collection of annotations and tools that are used to turn Kotlin properties into introspectable parameters. Parameters A collection of annotations and tools that are used to turn Kotlin properties into introspectable parameters. Parameters
are highly suitable for automatically generating user interfaces, but note that this is _not_ what `orx-parameters` does. are highly suitable for automatically generating user interfaces, but note that this is _not_ what `orx-parameters` does.
For an example (and a highly usable implementation) of generating interfaces from the annotations you are encouraged to check out [`orx-gui`](../orx-gui/README.md).
Currently orx-parameters supplies the following annotations: Currently orx-parameters supplies the following annotations:
- `DoubleParameter` - `DoubleParameter` for `Double` properties
- `IntParameter` - `IntParameter` for `Int` properties
- `BooleanParameter` - `BooleanParameter` for `Boolean` properties
- `TextParameter` - `TextParameter` for `String` properties
- `ColorParameter` for `ColorRGBa` properties
Additionally there is an `ActionParameter` that can be used to annotate functions without arguments.
## Annotation application ## Annotation application
@@ -24,6 +29,12 @@ val foo = object {
@BooleanParameter("a boolean parameter", order = 2) @BooleanParameter("a boolean parameter", order = 2)
var b = false var b = false
@ActionParameter("a simple action", order = 3)
fun actionFunction() {
// --
}
} }
```` ````

View File

@@ -1,8 +1,10 @@
package org.openrndr.extra.parameters package org.openrndr.extra.parameters
import kotlin.reflect.KCallable
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KVisibility import kotlin.reflect.KVisibility
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.findAnnotation
@@ -69,19 +71,19 @@ annotation class TextParameter(val label: String, val order: Int = Integer.MAX_V
annotation class ColorParameter(val label: String, val order: Int = Integer.MAX_VALUE) annotation class ColorParameter(val label: String, val order: Int = Integer.MAX_VALUE)
/** /**
* ButtonParameter annotation for button parameter * ActionParameter annotation for functions without arguments
* @property label a short description of the parameter * @property label a short description of the parameter
* @property order hint for where to place the parameter in user interfaces * @property order hint for where to place the parameter in user interfaces
*/ */
@Target(AnnotationTarget.PROPERTY) @Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class ButtonParameter(val label: String, val order: Int = Integer.MAX_VALUE) annotation class ActionParameter(val label: String, val order: Int = Integer.MAX_VALUE)
enum class ParameterType(val annotationClass: KClass<out Annotation>) { enum class ParameterType(val annotationClass: KClass<out Annotation>) {
Double(DoubleParameter::class), Double(DoubleParameter::class),
Int(IntParameter::class), Int(IntParameter::class),
Boolean(BooleanParameter::class), Boolean(BooleanParameter::class),
Button(ButtonParameter::class), Action(ActionParameter::class),
Text(TextParameter::class), Text(TextParameter::class),
Color(ColorParameter::class) Color(ColorParameter::class)
; ;
@@ -94,12 +96,12 @@ enum class ParameterType(val annotationClass: KClass<out Annotation>) {
} }
} }
/** /**
* Parameter summary class. This is used by [listParameters] as a way to report parameters in * Parameter summary class. This is used by [listParameters] as a way to report parameters in
* a unified form. * a unified form.
* @property property the property that received any of the parameter annotations * @property property a property that received any of the parameter annotations
* [BooleanParameter], [IntParameter], or [DoubleParameter] * [BooleanParameter], [IntParameter], or [DoubleParameter]
* @property function a function that received the [ActionParameter]
* @property label a label that describes the property * @property label a label that describes the property
* @property doubleRange a floating point based range in case [DoubleParameter] is used * @property doubleRange a floating point based range in case [DoubleParameter] is used
* @property intRange an integer range in case [IntParameter] is used * @property intRange an integer range in case [IntParameter] is used
@@ -108,7 +110,8 @@ enum class ParameterType(val annotationClass: KClass<out Annotation>) {
*/ */
class Parameter( class Parameter(
val parameterType: ParameterType, val parameterType: ParameterType,
val property: KMutableProperty1<out Any, Any?>, val property: KMutableProperty1<out Any, Any?>?,
val function: KCallable<Unit>?,
val label: String, val label: String,
val doubleRange: ClosedRange<Double>?, val doubleRange: ClosedRange<Double>?,
val intRange: IntRange?, val intRange: IntRange?,
@@ -119,7 +122,7 @@ class Parameter(
* List all parameters, (public var properties with a parameter annotation) * List all parameters, (public var properties with a parameter annotation)
*/ */
fun Any.listParameters(): List<Parameter> { fun Any.listParameters(): List<Parameter> {
return this::class.declaredMemberProperties.filter { return (this::class.declaredMemberProperties.filter {
!it.isConst && !it.isConst &&
it.visibility == KVisibility.PUBLIC && it.visibility == KVisibility.PUBLIC &&
it.annotations.map { it.annotationClass }.intersect(ParameterType.parameterAnnotationClasses).isNotEmpty() it.annotations.map { it.annotationClass }.intersect(ParameterType.parameterAnnotationClasses).isNotEmpty()
@@ -150,10 +153,6 @@ fun Any.listParameters(): List<Parameter> {
intRange = it.low..it.high intRange = it.low..it.high
order = it.order order = it.order
} }
is ButtonParameter -> {
label = it.label
order = it.order
}
is TextParameter -> { is TextParameter -> {
label = it.label label = it.label
order = it.order order = it.order
@@ -164,9 +163,34 @@ fun Any.listParameters(): List<Parameter> {
} }
} }
} }
Parameter(type Parameter(
?: error("no type"), it as KMutableProperty1<out Any, Any?>, label, doubleRange, intRange, precision, order) parameterType = type ?: error("no type"),
}.sortedBy { it.order } property = it as KMutableProperty1<out Any, Any?>,
function = null,
label = label,
doubleRange = doubleRange,
intRange = intRange,
precision = precision,
order = order
)
} + this::class.declaredMemberFunctions.filter {
it.findAnnotation<ActionParameter>() != null
}.map {
val annotation = it.findAnnotation<ActionParameter>()!!
val label = annotation.label
val order = annotation.order
val type = ParameterType.Action
Parameter(
type,
property = null,
function = it as KCallable<Unit>,
label = label,
doubleRange = null,
intRange = null,
precision = null,
order = order
)
}).sortedBy { it.order }
} }
fun Any.title() = this::class.findAnnotation<Description>()?.title fun Any.title() = this::class.findAnnotation<Description>()?.title

View File

@@ -1,9 +1,9 @@
import org.amshove.kluent.`should be equal to` import org.amshove.kluent.*
import org.amshove.kluent.shouldBeInRange
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.extra.parameters.* import org.openrndr.extra.parameters.*
import org.spekframework.spek2.Spek import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe import org.spekframework.spek2.style.specification.describe
import java.lang.IllegalArgumentException
val a = object { val a = object {
@DoubleParameter("a double scalar", 0.0, 1.0, order = 0) @DoubleParameter("a double scalar", 0.0, 1.0, order = 0)
@@ -15,8 +15,10 @@ val a = object {
@BooleanParameter("a boolean parameter", order = 2) @BooleanParameter("a boolean parameter", order = 2)
var b = false var b = false
@ButtonParameter("a button parameter", order = 3) @ActionParameter("an action parameter", order = 3)
var f = {} fun a() {
error("this is to test if this function can be called")
}
@TextParameter("a text parameter", order = 4) @TextParameter("a text parameter", order = 4)
var t = "test" var t = "test"
@@ -32,7 +34,7 @@ object TestAnnotations : Spek({
val list = a.listParameters() val list = a.listParameters()
list.size `should be equal to` 6 list.size `should be equal to` 6
list[0].property.name `should be equal to` "d" list[0].property?.name `should be equal to` "d"
list[0].parameterType `should be equal to` ParameterType.Double list[0].parameterType `should be equal to` ParameterType.Double
list[0].label `should be equal to` "a double scalar" list[0].label `should be equal to` "a double scalar"
list[0].doubleRange?.let { list[0].doubleRange?.let {
@@ -41,29 +43,41 @@ object TestAnnotations : Spek({
} }
list[0].precision `should be equal to` 3 list[0].precision `should be equal to` 3
list[1].property.name `should be equal to` "i" list[1].property?.name `should be equal to` "i"
list[1].parameterType `should be equal to` ParameterType.Int list[1].parameterType `should be equal to` ParameterType.Int
list[1].label `should be equal to` "an integer scalar" list[1].label `should be equal to` "an integer scalar"
list[1].intRange?.let { list[1].intRange?.let {
it.start `should be equal to` 1 it.first `should be equal to` 1
it.endInclusive `should be equal to` 100 it.last `should be equal to` 100
} }
list[2].property.name `should be equal to` "b" list[2].property?.name `should be equal to` "b"
list[2].parameterType `should be equal to` ParameterType.Boolean list[2].parameterType `should be equal to` ParameterType.Boolean
list[2].label `should be equal to` "a boolean parameter" list[2].label `should be equal to` "a boolean parameter"
list[2].precision `should be equal to` null list[2].precision `should be equal to` null
list[3].parameterType `should be equal to` ParameterType.Button list[3].parameterType `should be equal to` ParameterType.Action
list[3].property.name `should be equal to` "f" list[3].property `should be equal to` null
list[3].label `should be equal to` "a button parameter" list[3].label `should be equal to` "an action parameter"
/* test if we can call the annotated function, this is performed by raising an expected exception in the
function in the annotated function.
*/
invoking {
try {
list[3].function?.call(a)
} catch(e: java.lang.reflect.InvocationTargetException) {
/* this unpacks the exception that is wrapped in the ITExc */
throw(e.targetException)
}
} `should throw` IllegalStateException::class withMessage "this is to test if this function can be called"
list[4].parameterType `should be equal to` ParameterType.Text list[4].parameterType `should be equal to` ParameterType.Text
list[4].property.name `should be equal to` "t" list[4].property?.name `should be equal to` "t"
list[4].label `should be equal to` "a text parameter" list[4].label `should be equal to` "a text parameter"
list[5].parameterType `should be equal to` ParameterType.Color list[5].parameterType `should be equal to` ParameterType.Color
list[5].property.name `should be equal to` "c" list[5].property?.name `should be equal to` "c"
list[5].label `should be equal to` "a color parameter" list[5].label `should be equal to` "a color parameter"
} }
} }