diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..adc74adf --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,17 @@ +name: Gradle build + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Build with Gradle + run: ./gradlew build diff --git a/README.md b/README.md index 783d9995..ff2820f1 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,73 @@ -# DataForge plugins for visualisation +[![JetBrains Research](https://jb.gg/badges/research.svg)](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) -## Common visualisation objects +# DataForge Visualisation Platform -## JavaFX utilities for meta manipulations +This repository contains [DataForge](http://npm.mipt.ru/dataforge/) +(also [here](https://github.com/mipt-npm/dataforge-core)) components useful for visualization in +various scientific applications. Currently, the main application is 3D visualization for particle +physics experiments. -## 3D visualisation +The project is developed as a Kotlin multiplatform application, currently +targeting browser JavaScript and JVM. -Includes common discription and serializers, JavaFX and Three.js implementations. +Main features: +- 3D visualization of complex experimental set-ups +- Event display such as particle tracks, etc. +- Scales up to few hundred thousands of elements +- Camera move, rotate, zoom-in and zoom-out +- Object tree with property editor +- Settings export and import +- Multiple platform support + -## GDML bindings for 3D visualisation (to be moved to gdml project) +## Modules contained in this repository: + + +### dataforge-vis-common + +Common visualisation objects such as VisualObject and VisualGroup. + + +### dataforge-vis-spatial + +Includes common description and serializers for 3D visualisation, JavaFX and Three.js implementations. + + +### dataforge-vis-spatial-gdml + +GDML bindings for 3D visualisation (to be moved to gdml project). + + +### dataforge-vis-jsroot + +Some JSROOT bindings. + +Note: Currently, this part is experimental and put here for completeness. This module may not build. + + +### demo + +Several demonstrations of using the dataforge-vis framework: + +##### spatial-showcase + +Contains a simple demonstration (grid with a few shapes that you can rotate, move camera, etc.). + +To see the demo: run `demo/spatial-showcase/distribution/installJsDist` Gradle task, then open +`build/distribuions/spatial-showcase-js-0.1.0-dev/index.html` file in your browser. + +Other demos can be built similarly. + +##### muon-monitor + +A full-stack application example, showing the +[Muon Monitor](http://npm.mipt.ru/projects/physics.html#mounMonitor) experiment set-up. + +Includes server back-end generating events, as well as visualization front-end. + +To run full-stack app (both server and browser front-end), run +`demo/muon-monitor/application/run` task. + +##### gdml + +Visualization example for geometry defined as GDML file. diff --git a/build.gradle.kts b/build.gradle.kts index ada9bd4c..0378807d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,34 +1,38 @@ -val dataforgeVersion by extra("0.1.3") +import scientifik.useSerialization -plugins{ - val kotlinVersion = "1.3.50-eap-5" +val dataforgeVersion by extra("0.1.5-dev-6") + +plugins { + val kotlinVersion = "1.3.61" + val toolsVersion = "0.3.2" kotlin("jvm") version kotlinVersion apply false - id("kotlin2js") version kotlinVersion apply false id("kotlin-dce-js") version kotlinVersion apply false - id("org.jetbrains.kotlin.frontend") version "0.0.45" apply false - id("scientifik.mpp") version "0.1.4" apply false - id("scientifik.jvm") version "0.1.4" apply false - id("scientifik.js") version "0.1.4" apply false - id("scientifik.publish") version "0.1.4" apply false - id("org.openjfx.javafxplugin") version "0.0.7" apply false + id("scientifik.mpp") version toolsVersion apply false + id("scientifik.jvm") version toolsVersion apply false + id("scientifik.js") version toolsVersion apply false + id("scientifik.publish") version toolsVersion apply false + id("org.openjfx.javafxplugin") version "0.0.8" apply false } allprojects { repositories { mavenLocal() - jcenter() - maven("https://kotlin.bintray.com/kotlinx") - maven("http://npm.mipt.ru:8081/artifactory/gradle-dev-local") - maven("https://kotlin.bintray.com/js-externals") maven("https://dl.bintray.com/pdvrieze/maven") - maven("https://dl.bintray.com/kotlin/kotlin-eap") + maven("http://maven.jzy3d.org/releases") + maven("https://kotlin.bintray.com/js-externals") +// maven("https://dl.bintray.com/gbaldeck/kotlin") +// maven("https://dl.bintray.com/rjaros/kotlin") } group = "hep.dataforge" version = "0.1.0-dev" } +subprojects{ + this.useSerialization() +} + val githubProject by extra("dataforge-vis") val bintrayRepo by extra("dataforge") diff --git a/dataforge-vis-common/build.gradle.kts b/dataforge-vis-common/build.gradle.kts index e57c78bb..548c1be5 100644 --- a/dataforge-vis-common/build.gradle.kts +++ b/dataforge-vis-common/build.gradle.kts @@ -1,25 +1,50 @@ +import org.openjfx.gradle.JavaFXOptions +import scientifik.useSerialization + plugins { id("scientifik.mpp") -} - -scientifik{ - serialization = true + id("org.openjfx.javafxplugin") } val dataforgeVersion: String by rootProject.extra +//val kvisionVersion: String by rootProject.extra("2.0.0-M1") + +useSerialization() kotlin { + jvm{ + withJava() + } + sourceSets { - val commonMain by getting { + commonMain{ dependencies { api("hep.dataforge:dataforge-output:$dataforgeVersion") } } - val jsMain by getting { + jvmMain{ + dependencies { + api("no.tornado:tornadofx:1.7.19") + //api("no.tornado:tornadofx-controlsfx:0.1.1") + api("de.jensd:fontawesomefx-fontawesome:4.7.0-11"){ + exclude(group = "org.openjfx") + } + api("de.jensd:fontawesomefx-commons:11.0"){ + exclude(group = "org.openjfx") + } + } + } + jsMain{ dependencies { api("hep.dataforge:dataforge-output-html:$dataforgeVersion") - api(npm("text-encoding")) + api(npm("bootstrap","4.4.1")) + implementation(npm("jsoneditor")) + implementation(npm("file-saver")) } } } -} \ No newline at end of file +} + +configure { + modules("javafx.controls") +} diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualGroup.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualGroup.kt new file mode 100644 index 00000000..889cb0dc --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualGroup.kt @@ -0,0 +1,112 @@ +package hep.dataforge.vis.common + +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.* +import kotlinx.serialization.Transient + + +/** + * Abstract implementation of mutable group of [VisualObject] + */ +abstract class AbstractVisualGroup : AbstractVisualObject(), MutableVisualGroup { + + //protected abstract val _children: MutableMap + + /** + * A map of top level named children + */ + abstract override val children: Map + + override fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?) { + super.propertyChanged(name, before, after) + forEach { + it.propertyChanged(name, before, after) + } + } + + // TODO Consider renaming to `StructureChangeListener` (singular) + private data class StructureChangeListeners(val owner: Any?, val callback: (Name, VisualObject?) -> Unit) + + @Transient + private val structureChangeListeners = HashSet() + + /** + * Add listener for children change + */ + override fun onChildrenChange(owner: Any?, action: (Name, VisualObject?) -> Unit) { + structureChangeListeners.add(StructureChangeListeners(owner, action)) + } + + /** + * Remove children change listener + */ + override fun removeChildrenChangeListener(owner: Any?) { + structureChangeListeners.removeAll { it.owner === owner } + } + + /** + * Propagate children change event upwards + */ + protected fun childrenChanged(name: Name, child: VisualObject?) { + structureChangeListeners.forEach { it.callback(name, child) } + } + + /** + * Remove a child with given name token + */ + protected abstract fun removeChild(token: NameToken) + + /** + * Add, remove or replace child with given name + */ + protected abstract fun setChild(token: NameToken, child: VisualObject) + + /** + * Add a static child. Statics could not be found by name, removed or replaced + */ + protected open fun addStatic(child: VisualObject) = + setChild(NameToken("@static(${child.hashCode()})"), child) + + /** + * Recursively create a child group + */ + protected abstract fun createGroup(name: Name): MutableVisualGroup + + /** + * Add named or unnamed child to the group. If key is null the child is considered unnamed. Both key and value are not + * allowed to be null in the same time. If name is present and [child] is null, the appropriate element is removed. + */ + override fun set(name: Name, child: VisualObject?) { + when { + name.isEmpty() -> { + if (child != null) { + addStatic(child) + } + } + name.length == 1 -> { + val token = name.first()!! + if (child == null) { + removeChild(token) + } else { + setChild(token, child) + } + } + else -> { + //TODO add safety check + val parent = (get(name.cutLast()) as? MutableVisualGroup) ?: createGroup(name.cutLast()) + parent[name.last()!!.asName()] = child + } + } + structureChangeListeners.forEach { it.callback(name, child) } + } + + operator fun set(key: String, child: VisualObject?): Unit { + if (key.isBlank()) { + if(child!= null) { + addStatic(child) + } + } else { + set(key.toName(), child) + } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualObject.kt new file mode 100644 index 00000000..7701eb1c --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/AbstractVisualObject.kt @@ -0,0 +1,93 @@ +package hep.dataforge.vis.common + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.vis.common.VisualObject.Companion.STYLE_KEY +import kotlinx.serialization.Transient + +internal data class PropertyListener( + val owner: Any? = null, + val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit +) + +abstract class AbstractVisualObject : VisualObject { + + @Transient + override var parent: VisualObject? = null + + protected abstract var properties: Config? + + override var styles: List + get() = properties?.get(STYLE_KEY).stringList + set(value) { + //val allStyles = (field + value).distinct() + setProperty(STYLE_KEY, value) + updateStyles(value) + } + + protected fun updateStyles(names: List) { + names.mapNotNull { findStyle(it) }.asSequence() + .flatMap { it.items.asSequence() } + .distinctBy { it.key } + .forEach { + propertyChanged(it.key.asName(), null, it.value) + } + } + + /** + * The config is initialized and assigned on-demand. + * To avoid unnecessary allocations, one should access [properties] via [getProperty] instead. + */ + override val config: Config + get() = properties ?: Config().also { config -> + properties = config.apply { onChange(this, ::propertyChanged) } + } + + @Transient + private val listeners = HashSet() + + override fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?) { + if (before != after) { + for (l in listeners) { + l.action(name, before, after) + } + } + } + + override fun onPropertyChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) { + listeners.add(PropertyListener(owner, action)) + } + + override fun removeChangeListener(owner: Any?) { + listeners.removeAll { it.owner == owner } + } + + private var styleCache: Meta? = null + + /** + * Collect all styles for this object in a laminate + */ + protected val mergedStyles: Meta + get() = styleCache ?: findAllStyles().merge().also { + styleCache = it + } + + override fun allProperties(): Laminate = Laminate(properties, mergedStyles) + + override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { + return if (inherit) { + properties?.get(name) ?: mergedStyles[name] ?: parent?.getProperty(name, inherit) + } else { + properties?.get(name) ?: mergedStyles[name] + } + } +} + +//fun VisualObject.findStyle(styleName: Name): Meta? { +// if (this is VisualGroup) { +// val style = resolveStyle(styleName) +// if (style != null) return style +// } +// return parent?.findStyle(styleName) +//} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/Colors.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/Colors.kt index 0365e300..3335c086 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/Colors.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/Colors.kt @@ -1,7 +1,13 @@ package hep.dataforge.vis.common +import hep.dataforge.meta.* +import hep.dataforge.values.ValueType +import hep.dataforge.values.int +import kotlin.math.max + /** - * Taken from https://github.com/markaren/three.kt/blob/master/threejs-wrapper/src/main/kotlin/info/laht/threekt/math/ColorConstants.kt + * Definitions of common colors. Taken from + * https://github.com/markaren/three.kt/blob/master/threejs-wrapper/src/main/kotlin/info/laht/threekt/math/ColorConstants.kt */ object Colors { const val aliceblue = 0xF0F8FF @@ -174,4 +180,63 @@ object Colors { const val whitesmoke = 0xF5F5F5 const val yellow = 0xFFFF00 const val yellowgreen = 0x9ACD32 + + const val RED_KEY = "red" + const val GREEN_KEY = "green" + const val BLUE_KEY = "blue" + + /** + * Convert color represented as Meta to string of format #rrggbb + */ + fun fromMeta(item: MetaItem<*>): String { + return when (item) { + is MetaItem.NodeItem<*> -> { + val node = item.node + rgbToString( + node[RED_KEY].number?.toByte()?.toUByte() ?: 0u, + node[GREEN_KEY].number?.toByte()?.toUByte() ?: 0u, + node[BLUE_KEY].number?.toByte()?.toUByte() ?: 0u + ) + } + is MetaItem.ValueItem -> { + if (item.value.type == ValueType.NUMBER) { + rgbToString(item.value.int) + } else { + item.value.string + } + } + } + } + + /** + * Convert Int color to string of format #rrggbb + */ + fun rgbToString(rgb: Int): String { + val string = rgb.toString(16).padStart(6, '0') + return "#" + string.substring(max(0, string.length - 6)) + } + + /** + * Convert three bytes representing color to string of format #rrggbb + */ + fun rgbToString(red: UByte, green: UByte, blue: UByte): String { + fun colorToString(color: UByte): String { + return color.toString(16).padStart(2, '0') + } + return buildString { + append("#") + append(colorToString(red)) + append(colorToString(green)) + append(colorToString(blue)) + } + } + + /** + * Convert three bytes representing color to Meta + */ + fun rgbToMeta(r: UByte, g: UByte, b: UByte): Meta = buildMeta { + RED_KEY put r.toInt() + GREEN_KEY put g.toInt() + BLUE_KEY put b.toInt() + } } \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/StyleSheet.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/StyleSheet.kt new file mode 100644 index 00000000..fce0b422 --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/StyleSheet.kt @@ -0,0 +1,63 @@ +@file:UseSerializers(MetaSerializer::class) + +package hep.dataforge.vis.common + +import hep.dataforge.io.serialization.MetaSerializer +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.UseSerializers + +@Serializable +class StyleSheet() { + @Transient + internal var owner: VisualObject? = null + + constructor(owner: VisualObject) : this() { + this.owner = owner + } + + private val styleMap = HashMap() + + val items: Map get() = styleMap + + operator fun get(key: String): Meta? { + return styleMap[key] ?: (owner?.parent as? VisualGroup)?.styleSheet?.get(key) + } + + /** + * Define a style without notifying + */ + fun define(key: String, style: Meta?) { + if (style == null) { + styleMap.remove(key) + } else { + styleMap[key] = style + } + } + + operator fun set(key: String, style: Meta?) { + val oldStyle = styleMap[key] + define(key, style) + owner?.styleChanged(key, oldStyle, style) + } + + operator fun set(key: String, builder: MetaBuilder.() -> Unit) { + val newStyle = get(key)?.let { buildMeta(it, builder) } ?: buildMeta(builder) + set(key, newStyle.seal()) + } +} + +private fun VisualObject.styleChanged(key: String, oldStyle: Meta?, newStyle: Meta?) { + if (styles.contains(key)) { + //TODO optimize set concatenation + val tokens: Collection = ((oldStyle?.items?.keys ?: emptySet()) + (newStyle?.items?.keys ?: emptySet())) + .map { it.asName() } + tokens.forEach { parent?.propertyChanged(it, oldStyle?.get(it), newStyle?.get(it)) } + } + if (this is VisualGroup) { + this.forEach { it.styleChanged(key, oldStyle, newStyle) } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualGroup.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualGroup.kt index 144f26e6..467616c3 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualGroup.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualGroup.kt @@ -1,131 +1,89 @@ package hep.dataforge.vis.common -import hep.dataforge.meta.Config -import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.MetaItem -import hep.dataforge.names.Name -import hep.dataforge.names.toName +import hep.dataforge.names.* import hep.dataforge.provider.Provider -import kotlinx.serialization.Transient -import kotlin.collections.set -open class VisualGroup : AbstractVisualObject(), Iterable, Provider { - - protected open val namedChildren: MutableMap = HashMap() - protected open val unnamedChildren: MutableList = ArrayList() - - override var properties: Config? = null +/** + * Represents a group of [VisualObject] instances + */ +interface VisualGroup : Provider, Iterable, VisualObject { + /** + * A map of top level named children + */ + val children: Map override val defaultTarget: String get() = VisualObject.TYPE - override fun iterator(): Iterator = (namedChildren.values + unnamedChildren).iterator() + val styleSheet: StyleSheet? - override fun provideTop(target: String): Map { - return when (target) { - VisualObject.TYPE -> namedChildren + override fun provideTop(target: String): Map = + when (target) { + VisualObject.TYPE -> children.flatMap { (key, value) -> + val res: Map = if (value is VisualGroup) { + value.provideTop(target).mapKeys { key + it.key } + } else { + mapOf(key.asName() to value) + } + res.entries + }.associate { it.toPair() } + STYLE_TARGET -> styleSheet?.items?.mapKeys { it.key.toName() } ?: emptyMap() else -> emptyMap() } - } - override fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?) { - super.propertyChanged(name, before, after) - forEach { - it.propertyChanged(name, before, after) + + /** + * Iterate over children of this group + */ + override fun iterator(): Iterator = children.values.iterator() + + operator fun get(name: Name): VisualObject? { + return when { + name.isEmpty() -> this + name.length == 1 -> children[name.first()!!] + else -> (children[name.first()!!] as? VisualGroup)?.get(name.cutFirst()) } } - private data class Listener(val owner: Any?, val callback: (Name?, T?) -> Unit) - - @Transient - private val listeners = HashSet>() - /** - * Add listener for children change + * A fix for serialization bug that writes all proper parents inside the tree after deserialization */ - fun onChildrenChange(owner: Any?, action: (Name?, T?) -> Unit) { - listeners.add(Listener(owner, action)) + fun attachChildren() { + styleSheet?.owner = this + this.children.values.forEach { + it.parent = this + (it as? VisualGroup)?.attachChildren() + } } + companion object { + const val STYLE_TARGET = "style" + } +} + +data class StyleRef(val group: VisualGroup, val styleName: Name) + +val VisualGroup.isEmpty: Boolean get() = this.children.isEmpty() + +/** + * Mutable version of [VisualGroup] + */ +interface MutableVisualGroup : VisualGroup { + + /** + * Add listener for children structure change. + * @param owner the handler to properly remove listeners + * @param action First argument of the action is the name of changed child. Second argument is the new value of the object. + */ + fun onChildrenChange(owner: Any?, action: (Name, VisualObject?) -> Unit) /** * Remove children change listener */ - fun removeChildrenChangeListener(owner: Any?) { - listeners.removeAll { it.owner === owner } - } + fun removeChildrenChangeListener(owner: Any?) - /** - * Add named or unnamed child to the group. If key is [null] the child is considered unnamed. Both key and value are not - * allowed to be null in the same time. If name is present and [child] is null, the appropriate element is removed. - */ - operator fun set(name: Name?, child: T?) { - when { - name != null -> { - if (child == null) { - namedChildren.remove(name) - } else { - if (child.parent == null) { - child.parent = this - } else { - error("Can't reassign existing parent for $child") - } - namedChildren[name] = child - } - listeners.forEach { it.callback(name, child) } - } - child != null -> add(child) - else -> error("Both key and child element are empty") - } - } + operator fun set(name: Name, child: VisualObject?) +} - operator fun set(key: String?, child: T?) = set(key?.asName(), child) +operator fun VisualGroup.get(str: String?) = get(str?.toName() ?: Name.EMPTY) - /** - * Get named child by name - */ - operator fun get(name: Name): T? = namedChildren[name] - - /** - * Get named child by string - */ - operator fun get(key: String): T? = namedChildren[key.toName()] - - /** - * Get an unnamed child - */ - operator fun get(index: Int): T? = unnamedChildren[index] - - /** - * Append unnamed child - */ - fun add(child: T) { - if (child.parent == null) { - child.parent = this - } else { - error("Can't reassign existing parent for $child") - } - unnamedChildren.add(child) - listeners.forEach { it.callback(null, child) } - } - - /** - * remove unnamed child - */ - fun remove(child: VisualObject) { - unnamedChildren.remove(child) - listeners.forEach { it.callback(null, null) } - } - - protected fun MetaBuilder.updateChildren() { - //adding unnamed children - "unnamedChildren" to unnamedChildren.map { it.toMeta() } - //adding named children - namedChildren.forEach { - "children[${it.key}]" to it.value.toMeta() - } - } - - override fun MetaBuilder.updateMeta() { - updateChildren() - } -} \ No newline at end of file +fun MutableVisualGroup.removeAll() = children.keys.map { it.asName() }.forEach { this[it] = null } \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObject.kt index 6f131fe2..6aad894a 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObject.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObject.kt @@ -2,18 +2,20 @@ package hep.dataforge.vis.common import hep.dataforge.meta.* import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.names.toName import hep.dataforge.provider.Type import hep.dataforge.vis.common.VisualObject.Companion.TYPE import kotlinx.serialization.Transient -private fun Laminate.withTop(meta: Meta): Laminate = Laminate(listOf(meta) + layers) -private fun Laminate.withBottom(meta: Meta): Laminate = Laminate(layers + meta) +//private fun Laminate.withTop(meta: Meta): Laminate = Laminate(listOf(meta) + layers) +//private fun Laminate.withBottom(meta: Meta): Laminate = Laminate(layers + meta) /** * A root type for display hierarchy */ @Type(TYPE) -interface VisualObject : MetaRepr, Configurable { +interface VisualObject : Configurable { /** * The parent object of this one. If null, this one is a root. @@ -21,10 +23,17 @@ interface VisualObject : MetaRepr, Configurable { @Transient var parent: VisualObject? + /** + * All properties including styles and prototypes if present, but without inheritance + */ + fun allProperties(): Laminate + /** * Set property for this object */ - fun setProperty(name: Name, value: Any?) + fun setProperty(name: Name, value: Any?) { + config[name] = value + } /** * Get property including or excluding parent properties @@ -32,9 +41,11 @@ interface VisualObject : MetaRepr, Configurable { fun getProperty(name: Name, inherit: Boolean = true): MetaItem<*>? /** - * Manually trigger property changed event. If [name] is empty, notify that the whole object is changed + * Trigger property invalidation event. If [name] is empty, notify that the whole object is changed */ - fun propertyChanged(name: Name, before: MetaItem<*>? = null, after: MetaItem<*>? = null): Unit + fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?): Unit + + fun propertyInvalidated(name: Name) = propertyChanged(name, null, null) /** * Add listener triggering on property change @@ -46,65 +57,67 @@ interface VisualObject : MetaRepr, Configurable { */ fun removeChangeListener(owner: Any?) + /** + * List of names of styles applied to this object. Order matters. Not inherited + */ + var styles: List + companion object { const val TYPE = "visual" + val STYLE_KEY = "@style".asName() //const val META_KEY = "@meta" //const val TAGS_KEY = "@tags" + } } -internal data class MetaListener( - val owner: Any? = null, - val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit -) +/** + * Get [VisualObject] property using key as a String + */ +fun VisualObject.getProperty(key: String, inherit: Boolean = true): MetaItem<*>? = getProperty(key.toName(), inherit) -abstract class AbstractVisualObject: VisualObject { +/** + * Set [VisualObject] property using key as a String + */ +fun VisualObject.setProperty(key: String, value: Any?) = setProperty(key.toName(), value) - @Transient - override var parent: VisualObject? = null +/** + * Add style name to the list of styles to be resolved later. The style with given name does not necessary exist at the moment. + */ +fun VisualObject.useStyle(name: String) { + styles = styles + name +} - @Transient - private val listeners = HashSet() +//private tailrec fun VisualObject.topGroup(): VisualGroup? { +// val parent = this.parent +// return if (parent == null) { +// this as? VisualGroup +// } +// else { +// parent.topGroup() +// } +//} +// +///** +// * Add or update given style on a top-most reachable parent group and apply it to this object +// */ +//fun VisualObject.useStyle(name: String, builder: MetaBuilder.() -> Unit) { +// val styleName = name.toName() +// topGroup()?.updateStyle(styleName, builder) ?: error("Can't find parent group for $this") +// useStyle(styleName) +//} - override fun propertyChanged(name: Name, before: MetaItem<*>?, after: MetaItem<*>?) { - for (l in listeners) { - l.action(name, before, after) - } - } +tailrec fun VisualObject.findStyle(name: String): Meta? = + (this as? VisualGroup)?.styleSheet?.get(name) ?: parent?.findStyle(name) - override fun onPropertyChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) { - listeners.add(MetaListener(owner, action)) - } +fun VisualObject.findAllStyles(): Laminate = Laminate(styles.mapNotNull(::findStyle)) - override fun removeChangeListener(owner: Any?) { - listeners.removeAll { it.owner == owner } - } +//operator fun VisualObject.get(name: Name): VisualObject?{ +// return when { +// name.isEmpty() -> this +// this is VisualGroup -> this[name] +// else -> null +// } +//} - abstract var properties: Config? - override val config: Config - get() = properties ?: Config().also { config -> - properties = config - config.onChange(this, ::propertyChanged) - } - - override fun setProperty(name: Name, value: Any?) { - config[name] = value - } - - override fun getProperty(name: Name, inherit: Boolean): MetaItem<*>? { - return if (inherit) { - properties?.get(name) ?: parent?.getProperty(name, inherit) - } else { - properties?.get(name) - } - } - - protected open fun MetaBuilder.updateMeta() {} - - override fun toMeta(): Meta = buildMeta { - "type" to this::class.simpleName - "properties" to properties - updateMeta() - } -} \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegate.kt similarity index 63% rename from dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt rename to dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegate.kt index 748e537b..1dba2c7f 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegates.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualObjectDelegate.kt @@ -4,18 +4,18 @@ import hep.dataforge.meta.* import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.asName +import hep.dataforge.names.toName import hep.dataforge.values.Value import kotlin.jvm.JvmName import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty -fun String.asName() = NameToken(this).asName() /** * A delegate for display object properties */ -class DisplayObjectDelegate( +class VisualObjectDelegate( val key: Name?, val default: MetaItem<*>?, val inherited: Boolean @@ -35,82 +35,84 @@ class DisplayObjectDelegate( } } -class DisplayObjectDelegateWrapper( +class VisualObjectDelegateWrapper( + val obj: VisualObject, val key: Name?, val default: T, val inherited: Boolean, val write: Config.(name: Name, value: T) -> Unit = { name, value -> set(name, value) }, val read: (MetaItem<*>?) -> T? -) : ReadWriteProperty { +) : ReadWriteProperty { //private var cachedName: Name? = null - override fun getValue(thisRef: VisualObject, property: KProperty<*>): T { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { val name = key ?: property.name.asName() - return if (inherited) { - read(thisRef.getProperty(name)) - } else { - read(thisRef.config[name]) - } ?: default + return read(obj.getProperty(name,inherited))?:default } - override fun setValue(thisRef: VisualObject, property: KProperty<*>, value: T) { + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { val name = key ?: property.name.asName() - thisRef.config[name] = value + obj.config[name] = value } } fun VisualObject.value(default: Value? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.value } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.value } fun VisualObject.string(default: String? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.string } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.string } fun VisualObject.boolean(default: Boolean? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.boolean } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.boolean } fun VisualObject.number(default: Number? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.number } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.number } fun VisualObject.double(default: Double? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.double } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.double } fun VisualObject.int(default: Int? = null, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.int } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.int } fun VisualObject.node(key: String? = null, inherited: Boolean = true) = - DisplayObjectDelegateWrapper(key?.asName(), null, inherited) { it.node } + VisualObjectDelegateWrapper(this, key?.toName(), null, inherited) { it.node } fun VisualObject.item(key: String? = null, inherited: Boolean = true) = - DisplayObjectDelegateWrapper(key?.asName(), null, inherited) { it } + VisualObjectDelegateWrapper(this, key?.toName(), null, inherited) { it } //fun Configurable.spec(spec: Specification, key: String? = null) = ChildConfigDelegate(key) { spec.wrap(this) } @JvmName("safeString") fun VisualObject.string(default: String, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.string } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.string } @JvmName("safeBoolean") fun VisualObject.boolean(default: Boolean, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.boolean } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.boolean } @JvmName("safeNumber") fun VisualObject.number(default: Number, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.number } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.number } @JvmName("safeDouble") fun VisualObject.double(default: Double, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.double } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.double } @JvmName("safeInt") fun VisualObject.int(default: Int, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.asName(), default, inherited) { it.int } + VisualObjectDelegateWrapper(this, key?.toName(), default, inherited) { it.int } inline fun > VisualObject.enum(default: E, key: String? = null, inherited: Boolean = false) = - DisplayObjectDelegateWrapper(key?.let{ NameToken(it).asName()}, default, inherited) { item -> item.string?.let { enumValueOf(it) } } + VisualObjectDelegateWrapper( + this, + key?.let { NameToken(it).asName() }, + default, + inherited + ) { item -> item.string?.let { enumValueOf(it) } } //merge properties @@ -118,11 +120,11 @@ fun VisualObject.merge( key: String? = null, transformer: (Sequence>) -> T ): ReadOnlyProperty { - return object : ReadOnlyProperty { - override fun getValue(thisRef: VisualObject, property: KProperty<*>): T { - val name = key?.asName() ?: property.name.asName() + return object : ReadOnlyProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val name = key?.toName() ?: property.name.asName() val sequence = sequence> { - var thisObj: VisualObject? = thisRef + var thisObj: VisualObject? = this@merge while (thisObj != null) { thisObj.config[name]?.let { yield(it) } thisObj = thisObj.parent diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualPlugin.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualPlugin.kt index 933e4736..39a1abc0 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualPlugin.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/VisualPlugin.kt @@ -30,7 +30,8 @@ class VisualPlugin(meta: Meta) : AbstractPlugin(meta) { companion object : PluginFactory { override val tag: PluginTag = PluginTag(name = "visual", group = PluginTag.DATAFORGE_GROUP) override val type: KClass = VisualPlugin::class - override fun invoke(meta: Meta): VisualPlugin = VisualPlugin(meta) + + override fun invoke(meta: Meta, context: Context): VisualPlugin = VisualPlugin(meta) const val VISUAL_FACTORY_TYPE = "visual.factory" } diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/valueWidget.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/valueWidget.kt index fb4d5f32..8c45686f 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/valueWidget.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/common/valueWidget.kt @@ -3,12 +3,18 @@ package hep.dataforge.vis.common import hep.dataforge.descriptors.ValueDescriptor import hep.dataforge.meta.* +/** + * Extension property to access the "widget" key of [ValueDescriptor] + */ var ValueDescriptor.widget: Meta get() = this.config["widget"].node?: EmptyMeta set(value) { this.config["widget"] = value } +/** + * Extension property to access the "widget.type" key of [ValueDescriptor] + */ var ValueDescriptor.widgetType: String? get() = this["widget.type"].string set(value) { diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/Application.kt similarity index 63% rename from dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt rename to dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/Application.kt index 37b4c4c1..53734c31 100644 --- a/dataforge-vis-common/src/jsMain/kotlin/hep.dataforge.vis.hmr/HMR.kt +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/Application.kt @@ -1,14 +1,10 @@ -package hep.dataforge.vis.hmr +package hep.dataforge.js import kotlin.browser.document import kotlin.dom.hasClass external val module: Module -external interface Module { - val hot: Hot? -} - external interface Hot { val data: dynamic @@ -19,17 +15,31 @@ external interface Hot { fun dispose(callback: (data: dynamic) -> Unit) } -external fun require(name: String): dynamic - -abstract class ApplicationBase { - open val stateKeys: List get() = emptyList() - - abstract fun start(state: Map) - open fun dispose(): Map = emptyMap() +external interface Module { + val hot: Hot? } -fun startApplication(builder: () -> ApplicationBase) { - fun start(state: dynamic): ApplicationBase? { +/** + * Base interface for applications. + * + * Base interface for applications supporting Hot Module Replacement (HMR). + */ +interface Application { + /** + * Starting point for an application. + * @param state Initial state between Hot Module Replacement (HMR). + */ + fun start(state: Map) + + /** + * Ending point for an application. + * @return final state for Hot Module Replacement (HMR). + */ + fun dispose(): Map = emptyMap() +} + +fun startApplication(builder: () -> Application) { + fun start(state: dynamic): Application? { return if (document.body?.hasClass("testApp") == true) { val application = builder() @@ -42,7 +52,7 @@ fun startApplication(builder: () -> ApplicationBase) { } } - var application: ApplicationBase? = null + var application: Application? = null val state: dynamic = module.hot?.let { hot -> hot.accept() diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt new file mode 100644 index 00000000..6354a61c --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/js/jsExtra.kt @@ -0,0 +1,24 @@ +package hep.dataforge.js + +@JsName("require") +external fun requireJS(name: String): dynamic + +inline fun jsObject(builder: T.() -> Unit): T { + val obj: T = js("({})") as T + return obj.apply { + builder() + } +} + +inline fun js(builder: dynamic.() -> Unit): dynamic = jsObject(builder) + +//fun clone(obj: T) = objectAssign(jsObject {}, obj) + +//inline fun assign(obj: T, builder: T.() -> Unit) = clone(obj).apply(builder) + +fun toPlainObjectStripNull(obj: Any) = js { + for (key in Object.keys(obj)) { + val value = obj.asDynamic()[key] + if (value != null) this[key] = value + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/bootstrap.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/bootstrap.kt new file mode 100644 index 00000000..2f1e4595 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/bootstrap.kt @@ -0,0 +1,15 @@ +package hep.dataforge.vis.js.editor + +import kotlinx.html.TagConsumer +import kotlinx.html.js.div +import kotlinx.html.js.h3 +import org.w3c.dom.HTMLElement + +inline fun TagConsumer.card(title: String, crossinline block: TagConsumer.() -> Unit) { + div("card w-100") { + div("card-body") { + h3(classes = "card-title") { +title } + block() + } + } +} \ No newline at end of file diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsTree.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsTree.kt new file mode 100644 index 00000000..3cf4a985 --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsTree.kt @@ -0,0 +1,78 @@ +package hep.dataforge.vis.js.editor + +import hep.dataforge.names.Name +import hep.dataforge.names.plus +import hep.dataforge.vis.common.VisualGroup +import hep.dataforge.vis.common.VisualObject +import hep.dataforge.vis.common.isEmpty +import kotlinx.html.TagConsumer +import kotlinx.html.dom.append +import kotlinx.html.js.* +import org.w3c.dom.Element +import org.w3c.dom.HTMLElement +import org.w3c.dom.HTMLSpanElement +import kotlin.dom.clear + +fun Element.displayObjectTree( + obj: VisualObject, + clickCallback: (Name) -> Unit = {} +) { + clear() + append { + card("Object tree") { + subTree(Name.EMPTY, obj, clickCallback) + } + } +} + +private fun TagConsumer.subTree( + fullName: Name, + obj: VisualObject, + clickCallback: (Name) -> Unit +) { +// val fullName = parentName + token + val token = fullName.last()?.toString()?:"World" + + //display as node if any child is visible + if (obj is VisualGroup && obj.children.keys.any { !it.body.startsWith("@") }) { + lateinit var toggle: HTMLSpanElement + div("d-inline-block text-truncate") { + toggle = span("objTree-caret") + label("objTree-label") { + +token + onClickFunction = { clickCallback(fullName) } + } + } + val subtree = ul("objTree-subtree") + toggle.onclick = { + toggle.classList.toggle("objTree-caret-down") + subtree.apply { + //If expanded, add children dynamically + if (toggle.classList.contains("objTree-caret-down")) { + obj.children.entries + .filter { !it.key.toString().startsWith("@") } // ignore statics and other hidden children + .sortedBy { (it.value as? VisualGroup)?.isEmpty ?: true } + .forEach { (childToken, child) -> + append { + li().apply { + subTree(fullName + childToken, child, clickCallback) + } + } + } + } else { + // if not, clear them to conserve memory on very long lists + this.clear() + } + } + } + } else { + div("d-inline-block text-truncate") { + span("objTree-leaf") + label("objTree-label") { + +token + onClickFunction = { clickCallback(fullName) } + } + } + } +} + diff --git a/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsoneditor.kt b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsoneditor.kt new file mode 100644 index 00000000..8bfaf93a --- /dev/null +++ b/dataforge-vis-common/src/jsMain/kotlin/hep/dataforge/vis/js/editor/jsoneditor.kt @@ -0,0 +1,185 @@ +@file:Suppress( + "INTERFACE_WITH_SUPERCLASS", + "OVERRIDING_FINAL_MEMBER", + "RETURN_TYPE_MISMATCH_ON_OVERRIDE", + "CONFLICTING_OVERLOADS", + "EXTERNAL_DELEGATION" +) + +package hep.dataforge.vis.js.editor + +import org.w3c.dom.HTMLElement + +external interface Node { + var field: String + var value: String? get() = definedExternally; set(value) = definedExternally + var path: dynamic +} + +external interface NodeName { + var path: Array + var type: dynamic /* 'object' | 'array' */ + var size: Number +} + +external interface ValidationError { + var path: dynamic + var message: String +} + +external interface Template { + var text: String + var title: String + var className: String? get() = definedExternally; set(value) = definedExternally + var field: String + var value: Any +} + +external interface `T$6` { + var startFrom: Number + var options: Array +} + +external interface AutoCompleteOptions { + var confirmKeys: Array? get() = definedExternally; set(value) = definedExternally + var caseSensitive: Boolean? get() = definedExternally; set(value) = definedExternally +// var getOptions: AutoCompleteOptionsGetter? get() = definedExternally; set(value) = definedExternally +} + +external interface SelectionPosition { + var row: Number + var column: Number +} + +external interface SerializableNode { + var value: Any + var path: dynamic +} + +external interface Color { + var rgba: Array + var hsla: Array + var rgbString: String + var rgbaString: String + var hslString: String + var hslaString: String + var hex: String +} + +//external interface `T$0` { +// var field: Boolean +// var value: Boolean +//} +// +//external interface `T$1` { +// @nativeGetter +// operator fun get(key: String): String? +// +// @nativeSetter +// operator fun set(key: String, value: String) +//} + +//external interface Languages { +// @nativeGetter +// operator fun get(lang: String): `T$1`? +// +// @nativeSetter +// operator fun set(lang: String, value: `T$1`) +//} + +external interface JSONEditorOptions { +// var ace: AceAjax.Ace? get() = definedExternally; set(value) = definedExternally +// var ajv: Ajv? get() = definedExternally; set(value) = definedExternally + var onChange: (() -> Unit)? get() = definedExternally; set(value) = definedExternally + var onChangeJSON: ((json: Any) -> Unit)? get() = definedExternally; set(value) = definedExternally + var onChangeText: ((jsonString: String) -> Unit)? get() = definedExternally; set(value) = definedExternally + var onEditable: ((node: Node) -> dynamic)? get() = definedExternally; set(value) = definedExternally + var onError: ((error: Error) -> Unit)? get() = definedExternally; set(value) = definedExternally + var onModeChange: ((newMode: dynamic /* 'tree' | 'view' | 'form' | 'code' | 'text' */, oldMode: dynamic /* 'tree' | 'view' | 'form' | 'code' | 'text' */) -> Unit)? get() = definedExternally; set(value) = definedExternally + var onNodeName: ((nodeName: NodeName) -> String?)? get() = definedExternally; set(value) = definedExternally + var onValidate: ((json: Any) -> dynamic)? get() = definedExternally; set(value) = definedExternally + var escapeUnicode: Boolean? get() = definedExternally; set(value) = definedExternally + var sortObjectKeys: Boolean? get() = definedExternally; set(value) = definedExternally + var history: Boolean? get() = definedExternally; set(value) = definedExternally + var mode: dynamic /* 'tree' | 'view' | 'form' | 'code' | 'text' */ + var modes: Array? get() = definedExternally; set(value) = definedExternally + var name: String? get() = definedExternally; set(value) = definedExternally + var schema: Any? get() = definedExternally; set(value) = definedExternally + var schemaRefs: Any? get() = definedExternally; set(value) = definedExternally + var search: Boolean? get() = definedExternally; set(value) = definedExternally + var indentation: Number? get() = definedExternally; set(value) = definedExternally + var theme: String? get() = definedExternally; set(value) = definedExternally + var templates: Array