From eba509612966822915c1d44a09155d7d9beff898 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 2 Jun 2019 20:32:31 +0300 Subject: [PATCH] Working on configuration editor --- build.gradle.kts | 2 +- dataforge-vis-common/build.gradle.kts | 11 + .../kotlin/hep/dataforge/vis/DisplayList.kt | 67 ++++ .../kotlin/hep/dataforge/vis/DisplayObject.kt | 117 ++----- .../dataforge/vis/DisplayObjectDelegates.kt | 8 +- .../kotlin/hep/dataforge/vis/DisplayTree.kt | 43 +++ .../kotlin/hep/dataforge/vis/NamedObject.kt | 40 --- dataforge-vis-fx/build.gradle.kts | 23 ++ .../kotlin/hep/dataforge/vis/fx/FXPlugin.kt | 121 ++++++++ .../hep/dataforge/vis/fx/meta/ConfigEditor.kt | 173 +++++++++++ .../hep/dataforge/vis/fx/meta/ConfigFX.kt | 286 ++++++++++++++++++ .../hep/dataforge/vis/fx/meta/FXMeta.kt | 119 ++++++++ .../hep/dataforge/vis/fx/meta/MetaViewer.kt | 55 ++++ .../vis/fx/values/ColorValueChooser.kt | 47 +++ .../vis/fx/values/ComboBoxValueChooser.kt | 58 ++++ .../vis/fx/values/TextValueChooser.kt | 106 +++++++ .../dataforge/vis/fx/values/ValueCallback.kt | 23 ++ .../dataforge/vis/fx/values/ValueChooser.kt | 116 +++++++ .../vis/fx/values/ValueChooserBase.kt | 70 +++++ .../src/main/resources/img/df.png | Bin 0 -> 54732 bytes .../dataforge/vis/spatial/gdml/GDMLPlugin.kt | 3 +- .../kotlin/hep/dataforge/vis/spatial/Box.kt | 6 +- .../hep/dataforge/vis/spatial/Convex.kt | 5 +- .../hep/dataforge/vis/spatial/Extruded.kt | 5 +- .../dataforge/vis/spatial/displayObject3D.kt | 44 +-- .../hep/dataforge/vis/spatial/ConvexTest.kt | 7 +- settings.gradle.kts | 15 +- 27 files changed, 1385 insertions(+), 185 deletions(-) create mode 100644 dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayList.kt create mode 100644 dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayTree.kt delete mode 100644 dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/NamedObject.kt create mode 100644 dataforge-vis-fx/build.gradle.kts create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/FXPlugin.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigEditor.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigFX.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/FXMeta.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/MetaViewer.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ColorValueChooser.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ComboBoxValueChooser.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/TextValueChooser.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueCallback.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooser.kt create mode 100644 dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooserBase.kt create mode 100644 dataforge-vis-fx/src/main/resources/img/df.png diff --git a/build.gradle.kts b/build.gradle.kts index a49a9e8a..b0cc2184 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,4 @@ -val dataforgeVersion by extra("0.1.3-dev-1") +val dataforgeVersion by extra("0.1.3-dev-2") allprojects { repositories { diff --git a/dataforge-vis-common/build.gradle.kts b/dataforge-vis-common/build.gradle.kts index 900391e9..069181bb 100644 --- a/dataforge-vis-common/build.gradle.kts +++ b/dataforge-vis-common/build.gradle.kts @@ -9,6 +9,17 @@ kotlin { val commonMain by getting { dependencies { api("hep.dataforge:dataforge-output:$dataforgeVersion") + api("hep.dataforge:dataforge-output-metadata:$dataforgeVersion") + } + } + val jvmMain by getting{ + dependencies { + api("hep.dataforge:dataforge-output-jvm:$dataforgeVersion") + } + } + val jsMain by getting{ + dependencies { + api("hep.dataforge:dataforge-output-js:$dataforgeVersion") } } } diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayList.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayList.kt new file mode 100644 index 00000000..b03a1fc6 --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayList.kt @@ -0,0 +1,67 @@ +package hep.dataforge.vis + +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.meta.Styled + +internal data class InvalidationListener( + val owner: Any?, + val action: () -> Unit +) + +/** + * A [DisplayGroup] containing ordered list of elements + */ +class DisplayObjectList( + override val parent: DisplayObject? = null, +// override val type: String = DisplayObject.DEFAULT_TYPE, + meta: Meta = EmptyMeta +) : DisplayGroup { + private val _children = ArrayList() + + /** + * An ordered list of direct descendants + */ + val children: List get() = _children + + override fun iterator(): Iterator = children.iterator() + + + override val properties = Styled(meta) + private val listeners = HashSet() + + /** + * Add a child object and notify listeners + */ + fun addChild(obj: DisplayObject) { + _children.add(obj) + listeners.forEach { it.action() } + } + + + /** + * Remove a specific child and notify listeners + */ + fun removeChild(obj: DisplayObject) { + if (_children.remove(obj)) { + listeners.forEach { it.action } + } + } + + /** + * Add listener for children change + * TODO add detailed information into change listener + */ + fun onChildrenChange(owner: Any?, action: () -> Unit) { + listeners.add(InvalidationListener(owner, action)) + } + + + /** + * Remove children change listener + */ + fun removeChildrenChangeListener(owner: Any?) { + listeners.removeAll { it.owner === owner } + } +} + diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObject.kt index 504871f4..1f40a835 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObject.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObject.kt @@ -3,7 +3,6 @@ package hep.dataforge.vis import hep.dataforge.meta.* import hep.dataforge.names.Name import hep.dataforge.names.toName -import hep.dataforge.vis.DisplayObject.Companion.DEFAULT_TYPE import hep.dataforge.vis.DisplayObject.Companion.META_KEY import hep.dataforge.vis.DisplayObject.Companion.TAGS_KEY @@ -17,60 +16,34 @@ interface DisplayObject { */ val parent: DisplayObject? - /** - * The type of this object. Uses `.` notation. Empty type means untyped group - */ - val type: String +// /** +// * The type of this object. Uses `.` notation. Empty type means untyped group +// */ +// val type: String - val properties: Styled + val properties: MutableMeta<*> companion object { const val DEFAULT_TYPE = "" - const val TYPE_KEY = "@type" - const val CHILDREN_KEY = "@children" + //const val TYPE_KEY = "@type" + //const val CHILDREN_KEY = "@children" const val META_KEY = "@meta" const val TAGS_KEY = "@tags" } } -interface DisplayGroup : DisplayObject { - - val children: List - - /** - * Add a child object and notify listeners - */ - fun addChild(obj: DisplayObject) - - /** - * Remove a specific child and notify listeners - */ - fun removeChild(obj: DisplayObject) - - /** - * Add listener for children change - * TODO add detailed information into change listener - */ - fun onChildrenChange(owner: Any? = null, action: () -> Unit) - - /** - * Remove children change listener - */ - fun removeChildrenChangeListener(owner: Any? = null) -} - /** * Get the property of this display object of parent's if not found */ -tailrec operator fun DisplayObject.get(name: Name): MetaItem<*>? = properties[name] ?: parent?.get(name) +tailrec fun DisplayObject.getProperty(name: Name): MetaItem<*>? = properties[name] ?: parent?.getProperty(name) -operator fun DisplayObject.get(name: String) = get(name.toName()) +fun DisplayObject.getProperty(name: String): MetaItem<*>? = getProperty(name.toName()) /** * A change listener for [DisplayObject] configuration. */ fun DisplayObject.onChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) { - properties.style.onChange(owner, action) + properties.onChange(owner, action) parent?.onChange(owner, action) } @@ -78,7 +51,7 @@ fun DisplayObject.onChange(owner: Any?, action: (Name, before: MetaItem<*>?, aft * Remove all meta listeners with matching owners */ fun DisplayObject.removeChangeListener(owner: Any?) { - properties.style.removeListener(owner) + properties.removeListener(owner) parent?.removeChangeListener(owner) } @@ -90,61 +63,15 @@ val DisplayObject.meta: Meta get() = properties[META_KEY]?.node ?: EmptyMeta val DisplayObject.tags: List get() = properties[TAGS_KEY].stringList -internal data class ObjectListener( - val owner: Any?, - val action: () -> Unit -) - -/** - * Basic group of display objects - */ -open class DisplayNode( - override val parent: DisplayObject? = null, - override val type: String = DEFAULT_TYPE, - meta: Meta = EmptyMeta -) : DisplayGroup { - - private val _children = ArrayList() - override val children: List get() = _children - override val properties = Styled(meta) - private val listeners = HashSet() - - override fun addChild(obj: DisplayObject) { -// val before = _children[name] -// if (obj == null) { -// _children.remove(name) -// } else { -// _children[name] = obj -// } -// listeners.forEach { it.action(name, before, obj) } - _children.add(obj) - listeners.forEach { it.action() } - } - - override fun removeChild(obj: DisplayObject) { - if (_children.remove(obj)) { - listeners.forEach { it.action } - } - } - - override fun onChildrenChange(owner: Any?, action: () -> Unit) { - listeners.add(ObjectListener(owner, action)) - } - - - override fun removeChildrenChangeListener(owner: Any?) { - listeners.removeAll { it.owner === owner } - } -} - -/** - * Basic [DisplayObject] leaf element - */ -open class DisplayLeaf( - override val parent: DisplayObject?, - override val type: String, - meta: Meta = EmptyMeta -) : DisplayObject { - final override val properties = Styled(meta) -} +///** +// * Basic [DisplayObject] leaf element +// */ +//open class DisplayLeaf( +// override val parent: DisplayObject?, +//// override val type: String, +// meta: Meta = EmptyMeta +//) : DisplayObject { +// final override val properties = Styled(meta) +//} +interface DisplayGroup: DisplayObject, Iterable diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObjectDelegates.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObjectDelegates.kt index 5b11d5e4..2f444b46 100644 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObjectDelegates.kt +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayObjectDelegates.kt @@ -20,7 +20,7 @@ class DisplayObjectDelegate( override fun getValue(thisRef: DisplayObject, property: KProperty<*>): MetaItem<*>? { val name = key ?: property.name.toName() return if (inherited) { - thisRef[name] + thisRef.getProperty(name) } else { thisRef.properties[name] } ?: default @@ -28,7 +28,7 @@ class DisplayObjectDelegate( override fun setValue(thisRef: DisplayObject, property: KProperty<*>, value: MetaItem<*>?) { val name = key ?: property.name.toName() - thisRef.properties.style[name] = value + thisRef.properties[name] = value } } @@ -42,7 +42,7 @@ class DisplayObjectDelegateWrapper( override fun getValue(thisRef: DisplayObject, property: KProperty<*>): T { val name = key ?: property.name.toName() return if (inherited) { - read(thisRef[name]) + read(thisRef.getProperty(name)) } else { read(thisRef.properties[name]) } ?: default @@ -50,7 +50,7 @@ class DisplayObjectDelegateWrapper( override fun setValue(thisRef: DisplayObject, property: KProperty<*>, value: T) { val name = key ?: property.name.toName() - thisRef.properties.style.write(name, value) + thisRef.properties[name] = value } } diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayTree.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayTree.kt new file mode 100644 index 00000000..116cbe52 --- /dev/null +++ b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/DisplayTree.kt @@ -0,0 +1,43 @@ +package hep.dataforge.vis + +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken + +/** + * A navigable hierarchical display node + */ +interface DisplayTree : DisplayGroup { + operator fun get(nameToken: NameToken): DisplayObject? +} + +interface MutableDisplayTree : DisplayTree { + operator fun set(nameToken: NameToken, group: DisplayObject) +} + +/** + * Recursively get a child + */ +tailrec operator fun DisplayTree.get(name: Name): DisplayObject? = when (name.length) { + 0 -> this + 1 -> this[name[0]] + else -> name.first()?.let { this[it] as? DisplayTree }?.get(name.cutFirst()) +} + + +/** + * Set given object creating intermediate empty groups if needed + * @param name - the full name of a child + * @param objFactory - a function that creates child object from parent (to avoid mutable parent parameter) + */ +fun MutableDisplayTree.set(name: Name, objFactory: (parent: DisplayObject) -> DisplayObject): Unit = + when (name.length) { + 0 -> error("Can't set object with empty name") + 1 -> set(name[0], objFactory(this)) + else -> (this[name.first()!!] ?: DisplayObjectList(this)).run { + if (this is MutableDisplayTree) { + this.set(name.cutFirst(), objFactory) + } else { + error("Can't assign child to a leaf element $this") + } + } + } \ No newline at end of file diff --git a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/NamedObject.kt b/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/NamedObject.kt deleted file mode 100644 index bc15d9e6..00000000 --- a/dataforge-vis-common/src/commonMain/kotlin/hep/dataforge/vis/NamedObject.kt +++ /dev/null @@ -1,40 +0,0 @@ -package hep.dataforge.vis - -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken - -interface NamedObject : DisplayObject { - val name: String - - operator fun get(nameToken: NameToken): DisplayGroup? - - operator fun set(nameToken: NameToken, group: DisplayGroup) -} - -/** - * Recursively get a child - */ -tailrec operator fun NamedObject.get(name: Name): DisplayObject? = when (name.length) { - 0 -> this - 1 -> this[name[0]] - else -> name.first()?.let { this[it] as? NamedObject }?.get(name.cutFirst()) -} - - -/** - * Set given object creating intermediate empty groups if needed - * @param name - the full name of a child - * @param objFactory - a function that creates child object from parent (to avoid mutable parent parameter) - */ -fun NamedObject.set(name: Name, objFactory: (parent: DisplayObject) -> DisplayGroup): Unit = when (name.length) { - 0 -> error("Can't set object with empty name") - 1 -> set(name[0], objFactory(this)) - else -> (this[name.first()!!] ?: DisplayNode(this)) - .run { - if (this is NamedObject) { - this.set(name.cutFirst(), objFactory) - } else { - error("Can't assign child to a leaf element $this") - } - } -} \ No newline at end of file diff --git a/dataforge-vis-fx/build.gradle.kts b/dataforge-vis-fx/build.gradle.kts new file mode 100644 index 00000000..a1a13be8 --- /dev/null +++ b/dataforge-vis-fx/build.gradle.kts @@ -0,0 +1,23 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.openjfx.gradle.JavaFXOptions + +plugins { + kotlin("jvm") + id("org.openjfx.javafxplugin") +} + +dependencies { + api(project(":dataforge-vis-common")) + api("no.tornado:tornadofx:1.7.18") + api("no.tornado:tornadofx-controlsfx:0.1") +} + +configure { + modules("javafx.controls") +} + +tasks.withType { + kotlinOptions{ + jvmTarget = "1.8" + } +} \ No newline at end of file diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/FXPlugin.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/FXPlugin.kt new file mode 100644 index 00000000..01a873fb --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/FXPlugin.kt @@ -0,0 +1,121 @@ +package hep.dataforge.vis.fx + +import hep.dataforge.context.* +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.meta.boolean +import javafx.application.Application +import javafx.application.Platform +import javafx.collections.FXCollections +import javafx.collections.ObservableSet +import javafx.collections.SetChangeListener +import javafx.scene.Scene +import javafx.scene.image.Image +import javafx.scene.image.ImageView +import javafx.stage.Stage +import tornadofx.* +import kotlin.reflect.KClass + +/** + * Plugin holding JavaFX application instance and its root stage + */ +class FXPlugin(meta: Meta = EmptyMeta) : AbstractPlugin(meta) { + override val tag: PluginTag get() = Companion.tag + + private val stages: ObservableSet = FXCollections.observableSet() + + val consoleMode: Boolean by meta.boolean(true) + + init { + if (consoleMode) { + stages.addListener(SetChangeListener { change -> + if (change.set.isEmpty()) { + Platform.setImplicitExit(true) + } else { + Platform.setImplicitExit(false) + } + }) + } + } + + /** + * Wait for application and toolkit to start if needed + */ + override fun attach(context: Context) { + super.attach(context) + if (FX.getApplication(FX.defaultScope) == null) { + if (consoleMode) { + Thread { + context.logger.debug("Starting FX application surrogate") + launch() + }.apply { + name = "${context.name} FX application thread" + start() + } + + while (!FX.initialized.get()) { + if (Thread.interrupted()) { + throw RuntimeException("Interrupted application start") + } + } + Platform.setImplicitExit(false) + } else { + throw RuntimeException("FX Application not defined") + } + } + } + + /** + * Define an application to use in this context + */ + fun setApp(app: Application, stage: Stage) { + FX.registerApplication(FX.defaultScope, app, stage) + } + + /** + * Show something in a pre-constructed stage. Blocks thread until stage is created + * + * @param cons + */ + fun display(action: Stage.() -> Unit) { + runLater { + val stage = Stage() + stage.initOwner(FX.primaryStage) + stage.action() + stage.show() + stages.add(stage) + stage.setOnCloseRequest { stages.remove(stage) } + } + } + + fun display(component: UIComponent, width: Double = 800.0, height: Double = 600.0) { + display { + scene = Scene(component.root, width, height) + title = component.title + } + } + + companion object : PluginFactory { + override val type: KClass = FXPlugin::class + override val tag: PluginTag = PluginTag("vis.fx", group = PluginTag.DATAFORGE_GROUP) + override fun invoke(meta: Meta): FXPlugin = FXPlugin(meta) + } + +} + +val dfIcon: Image = Image(Global::class.java.getResourceAsStream("/img/df.png")) +val dfIconView = ImageView(dfIcon) + +/** + * An application surrogate without any visible primary stage + */ +class ApplicationSurrogate : App() { + override fun start(stage: Stage) { + FX.registerApplication(this, stage) + FX.initialized.value = true + } +} + +fun Context.display(width: Double = 800.0, height: Double = 600.0, component: () -> UIComponent) { + plugins.getOrLoad().display(component(), width, height) +} \ No newline at end of file diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigEditor.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigEditor.kt new file mode 100644 index 00000000..5cafce9a --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigEditor.kt @@ -0,0 +1,173 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.meta + +import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.Config +import hep.dataforge.names.NameToken +import hep.dataforge.vis.fx.dfIconView +import hep.dataforge.vis.fx.values.ValueChooser +import javafx.scene.control.* +import javafx.scene.control.cell.TextFieldTreeTableCell +import javafx.scene.layout.Priority +import javafx.scene.paint.Color +import javafx.scene.text.Text +import org.controlsfx.glyphfont.Glyph +import tornadofx.* + +/** + * FXML Controller class + * + * @author Alexander Nozik + */ +class ConfigEditor( + val configuration: Config, + title: String = "Configuration editor", + val descriptor: NodeDescriptor? = null +) : Fragment(title = title, icon = dfIconView) { + + val filter: (FXMeta) -> Boolean = { cfg -> + when (cfg) { + is FXMetaNode<*> -> !(cfg.descriptor?.tags?.contains(NO_CONFIGURATOR_TAG) ?: false) + is FXMetaValue -> !(cfg.descriptor?.tags?.contains(NO_CONFIGURATOR_TAG) ?: false) + } + } + + override val root = borderpane { + center = treetableview { + root = TreeItem(FXMeta.root(configuration, descriptor)) + root.isExpanded = true + sortMode = TreeSortMode.ALL_DESCENDANTS + columnResizePolicy = TreeTableView.CONSTRAINED_RESIZE_POLICY + column("Name", FXMeta::name) { + setCellFactory { + object : TextFieldTreeTableCell() { + override fun updateItem(item: NameToken?, empty: Boolean) { + super.updateItem(item, empty) + contextMenu?.items?.removeIf { it.text == "Remove" } + if (!empty) { + if (treeTableRow.item != null) { + textFillProperty().bind(treeTableRow.item.hasValue.objectBinding { + if (it == true) { + Color.BLACK + } else { + Color.GRAY + } + }) + if (treeTableRow.treeItem.value.hasValue.get()) { + contextmenu { + item("Remove") { + action { + treeTableRow.item.remove() + } + } + } + } + } + } + } + } + } + } + + column("Value") { param: TreeTableColumn.CellDataFeatures -> + param.value.valueProperty() + }.setCellFactory { + ValueCell() + } + + column("Description") { param: TreeTableColumn.CellDataFeatures -> param.value.value.descriptionProperty } + .setCellFactory { param: TreeTableColumn -> + val cell = TreeTableCell() + val text = Text() + cell.graphic = text + cell.prefHeight = Control.USE_COMPUTED_SIZE + text.wrappingWidthProperty().bind(param.widthProperty()) + text.textProperty().bind(cell.itemProperty()) + cell + } + } + } + + private fun showNodeDialog(): String? { + val dialog = TextInputDialog() + dialog.title = "Node name selection" + dialog.contentText = "Enter a name for new node: " + dialog.headerText = null + + val result = dialog.showAndWait() + return result.orElse(null) + } + + private fun showValueDialog(): String? { + val dialog = TextInputDialog() + dialog.title = "Value name selection" + dialog.contentText = "Enter a name for new value: " + dialog.headerText = null + + val result = dialog.showAndWait() + return result.orElse(null) + } + + private inner class ValueCell : TreeTableCell() { + + public override fun updateItem(item: FXMeta?, empty: Boolean) { + if (!empty) { + if (item != null) { + when (item) { + is FXMetaValue -> { + text = null + val chooser = ValueChooser.build(item.valueProperty, item.descriptor) { + item.set(it) + } + graphic = chooser.node + } + is FXMetaNode<*> -> { + item as FXMetaNode + + text = null + graphic = hbox { + button("node", Glyph("FontAwesome", "PLUS_CIRCLE")) { + hgrow = Priority.ALWAYS + maxWidth = Double.POSITIVE_INFINITY + action { + showNodeDialog()?.let { + item.addNode(it) + } + } + } + button("value", Glyph("FontAwesome", "PLUS_SQUARE")) { + hgrow = Priority.ALWAYS + maxWidth = Double.POSITIVE_INFINITY + action { + showValueDialog()?.let { + item.addValue(it) + } + } + } + } + } + } + + } else { + text = null + graphic = null + } + } else { + text = null + graphic = null + } + } + + } + + companion object { + /** + * The tag not to display node or value in configurator + */ + const val NO_CONFIGURATOR_TAG = "nocfg" + } +} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigFX.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigFX.kt new file mode 100644 index 00000000..fc06e4e7 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/ConfigFX.kt @@ -0,0 +1,286 @@ +//package hep.dataforge.vis.fx.meta +// +//import hep.dataforge.descriptors.NodeDescriptor +//import hep.dataforge.descriptors.ValueDescriptor +//import hep.dataforge.meta.Config +//import hep.dataforge.meta.Meta +//import hep.dataforge.names.Name +//import hep.dataforge.values.Null +//import hep.dataforge.values.Value +//import javafx.beans.binding.StringBinding +//import javafx.beans.property.SimpleObjectProperty +//import javafx.beans.property.SimpleStringProperty +//import javafx.beans.value.ObservableBooleanValue +//import javafx.beans.value.ObservableStringValue +//import javafx.collections.FXCollections +//import javafx.collections.ObservableList +//import javafx.scene.control.TreeItem +//import tornadofx.* +// +//class ConfigTreeItem(configFX: ConfigFX) : TreeItem(configFX) { +// init { +// this.children.bind(value.children) { ConfigTreeItem(it) } +// } +// +// override fun isLeaf(): Boolean = value is ConfigFXValue +//} +// +// +///** +// * A node, containing relative representation of configuration node and description +// * Created by darksnake on 01-May-17. +// */ +//sealed class ConfigFX(name: String) { +// +// val nameProperty = SimpleStringProperty(name) +// val name by nameProperty +// +// val parentProperty = SimpleObjectProperty() +// val parent by parentProperty +// +// abstract val hasValueProperty: ObservableBooleanValue +// //abstract val hasDefaultProperty: ObservableBooleanValue +// +// abstract val descriptionProperty: ObservableStringValue +// +// abstract val children: ObservableList +// +// /** +// * remove itself from parent +// */ +// abstract fun remove() +// +// abstract fun invalidate() +//} +// +// +///** +// * Tree item for node +// * Created by darksnake on 30-Apr-17. +// */ +//open class ConfigFXNode(name: String, parent: ConfigFXNode? = null) : ConfigFX(name) { +// +// final override val hasValueProperty = parentProperty.booleanBinding(nameProperty) { +// it?.config?.hasMeta(this.name) ?: false +// } +// +// /** +// * A descriptor that could be manually set to the node +// */ +// val descriptorProperty = SimpleObjectProperty() +// +// /** +// * Actual descriptor which holds value inferred from parrent +// */ +// private val actualDescriptor = objectBinding(descriptorProperty, parentProperty, nameProperty) { +// value ?: parent?.descriptor?.getNodeDescriptor(name) +// } +// +// val descriptor: NodeDescriptor? by actualDescriptor +// +// val configProperty = SimpleObjectProperty() +// +// private val actualConfig = objectBinding(configProperty, parentProperty, nameProperty) { +// value ?: parent?.config?.getMetaList(name)?.firstOrNull() +// } +// +// val config: Config? by actualConfig +// +// final override val descriptionProperty: ObservableStringValue = stringBinding(actualDescriptor) { +// value?.info ?: "" +// } +// +// override val children: ObservableList = FXCollections.observableArrayList() +// +// init { +// parentProperty.set(parent) +// hasValueProperty.onChange { +// parent?.hasValueProperty?.invalidate() +// } +// invalidate() +// } +// +// /** +// * Get existing configuration node or create and attach new one +// * +// * @return +// */ +// private fun getOrBuildNode(): Config { +// return config ?: if (parent == null) { +// throw RuntimeException("The configuration for root node is note defined") +// } else { +// parent.getOrBuildNode().requestNode(name) +// } +// } +// +// fun addValue(name: String) { +// getOrBuildNode().setValue(name, Null) +// } +// +// fun setValue(name: String, value: Value) { +// getOrBuildNode().setValue(name, value) +// } +// +// fun removeValue(valueName: String) { +// config?.removeValue(valueName) +// children.removeIf { it.name == name } +// } +// +// fun addNode(name: String) { +// getOrBuildNode().requestNode(name) +// } +// +// fun removeNode(name: String) { +// config?.removeNode(name) +// } +// +// override fun remove() { +// //FIXME does not work on multinodes +// parent?.removeNode(name) +// invalidate() +// } +// +// final override fun invalidate() { +// actualDescriptor.invalidate() +// actualConfig.invalidate() +// hasValueProperty.invalidate() +// +// val nodeNames = ArrayList() +// val valueNames = ArrayList() +// +// config?.apply { +// nodeNames.addAll(this.nodeNames.toList()) +// valueNames.addAll(this.valueNames.toList()) +// } +// +// descriptor?.apply { +// nodeNames.addAll(childrenDescriptors().keys) +// valueNames.addAll(valueDescriptors().keys) +// } +// +// //removing old values +// children.removeIf { !(valueNames.contains(it.name) || nodeNames.contains(it.name)) } +// +// valueNames.forEach { name -> +// children.find { it.name == name }?.invalidate().orElse { +// children.add(ConfigFXValue(name, this)) +// } +// } +// +// nodeNames.forEach { name -> +// children.find { it.name == name }?.invalidate().orElse { +// children.add(ConfigFXNode(name, this)) +// } +// } +// children.sortBy { it.name } +// } +// +// fun updateValue(path: Name, value: Value?) { +// when { +// path.length == 0 -> kotlin.error("Path never could be empty when updating value") +// path.length == 1 -> { +// val hasDescriptor = descriptor?.getValueDescriptor(path) != null +// if (value == null && !hasDescriptor) { +// //removing the value if it is present +// children.removeIf { it.name == path.unescaped } +// } else { +// //invalidating value if it is present +// children.find { it is ConfigFXValue && it.name == path.unescaped }?.invalidate().orElse { +// //adding new node otherwise +// children.add(ConfigFXValue(path.unescaped, this)) +// } +// } +// } +// path.length > 1 -> children.filterIsInstance().find { it.name == path.first.unescaped }?.updateValue( +// path.cutFirst(), +// value +// ) +// } +// } +// +// fun updateNode(path: Name, list: List) { +// when { +// path.isEmpty() -> invalidate() +// path.length == 1 -> { +// val hasDescriptor = descriptor?.getNodeDescriptor(path.unescaped) != null +// if (list.isEmpty() && !hasDescriptor) { +// children.removeIf { it.name == path.unescaped } +// } else { +// children.find { it is ConfigFXNode && it.name == path.unescaped }?.invalidate().orElse { +// children.add(ConfigFXNode(path.unescaped, this)) +// } +// } +// } +// else -> children.filterIsInstance().find { it.name == path.first.toString() }?.updateNode( +// path.cutFirst(), +// list +// ) +// } +// } +//} +// +//class ConfigFXRoot(rootConfig: Config, rootDescriptor: NodeDescriptor? = null) : ConfigFXNode(rootConfig.name), +// ConfigChangeListener { +// +// init { +// configProperty.set(rootConfig) +// descriptorProperty.set(rootDescriptor) +// rootConfig.addListener(this) +// invalidate() +// } +// +// override fun notifyValueChanged(name: Name, oldItem: Value?, newItem: Value?) { +// updateValue(name, newItem) +// } +// +// override fun notifyNodeChanged(nodeName: Name, oldItem: List, newItem: List) { +// updateNode(nodeName, newItem) +// } +//} +// +// +///** +// * Created by darksnake on 01-May-17. +// */ +//class ConfigFXValue(name: String, parent: ConfigFXNode) : ConfigFX(name) { +// +// init { +// parentProperty.set(parent) +// } +// +// override val hasValueProperty = parentProperty.booleanBinding(nameProperty) { +// it?.config?.hasValue(this.name) ?: false +// } +// +// +// override val children: ObservableList = FXCollections.emptyObservableList() +// +// val descriptor: ValueDescriptor? = parent.descriptor?.values[name] +// +// override val descriptionProperty: ObservableStringValue = object : StringBinding() { +// override fun computeValue(): String { +// return descriptor?.info ?: "" +// } +// } +// +// val valueProperty = parentProperty.objectBinding(nameProperty) { +// parent.config?.optValue(name).nullable ?: descriptor?.default +// } +// +// var value: Value +// set(value) { +// parent?.setValue(name, value) +// } +// get() = valueProperty.value ?: Value.NULL +// +// +// override fun remove() { +// parent?.removeValue(name) +// invalidate() +// } +// +// override fun invalidate() { +// valueProperty.invalidate() +// hasValueProperty.invalidate() +// } +//} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/FXMeta.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/FXMeta.kt new file mode 100644 index 00000000..2b33a767 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/FXMeta.kt @@ -0,0 +1,119 @@ +package hep.dataforge.vis.fx.meta + +import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.* +import hep.dataforge.names.NameToken +import hep.dataforge.names.asName +import hep.dataforge.values.Value +import javafx.beans.binding.ListBinding +import javafx.beans.binding.ObjectBinding +import javafx.beans.property.SimpleObjectProperty +import javafx.beans.value.ObservableBooleanValue +import javafx.beans.value.ObservableStringValue +import javafx.collections.ObservableList +import tornadofx.* + +sealed class FXMeta { + abstract val name: NameToken + abstract val parent: FXMetaNode<*>? + abstract val descriptionProperty: ObservableStringValue + + abstract val hasValue: ObservableBooleanValue + + companion object { + fun > root(node: M, descriptor: NodeDescriptor? = null): FXMetaNode = + FXMetaNode(NameToken("root"), null, node, descriptor) + + fun root(node: Meta, descriptor: NodeDescriptor? = null): FXMetaNode = + root(node.seal(), descriptor) + } +} + +class FXMetaNode>( + override val name: NameToken, + override val parent: FXMetaNode?, + node: M? = null, + descriptor: NodeDescriptor? = null +) : FXMeta() { + + /** + * A descriptor that could be manually set to the node + */ + val descriptorProperty = SimpleObjectProperty(descriptor) + + /** + * Actual descriptor which holds value inferred from parrent + */ + private val actualDescriptorProperty = objectBinding(descriptorProperty) { + value ?: parent?.descriptor?.nodes?.get(this@FXMetaNode.name.body) + } + + val descriptor: NodeDescriptor? by actualDescriptorProperty + + private val innerNodeProperty = SimpleObjectProperty(node) + + val nodeProperty: ObjectBinding = objectBinding(innerNodeProperty) { + value ?: parent?.node?.get(this@FXMetaNode.name.asName()).node + } + + val node: M? by nodeProperty + + override val descriptionProperty = descriptorProperty.stringBinding { it?.info ?: "" } + + override val hasValue: ObservableBooleanValue = nodeProperty.booleanBinding { it != null } + + val children = object : ListBinding() { + override fun computeValue(): ObservableList { + TODO() + } + } +} + +class FXMetaValue( + override val name: NameToken, + override val parent: FXMetaNode<*>, + value: Value? = null +) : FXMeta() { + + val descriptorProperty = parent.descriptorProperty.objectBinding { + it?.values?.get(name.body) + } + + /** + * A descriptor that could be manually set to the node + */ + val descriptor by descriptorProperty + + private val innerValueProperty = SimpleObjectProperty(value) + + val valueProperty = descriptorProperty.objectBinding { descriptor -> + parent.node[name].value ?: descriptor?.default + } + + override val hasValue: ObservableBooleanValue = valueProperty.booleanBinding { it != null } + + val value by valueProperty + + override val descriptionProperty = descriptorProperty.stringBinding { it?.info ?: "" } +} + +fun > FXMetaNode.remove(name: NameToken) { + node?.remove(name.asName()) + children.invalidate() +} + +fun FXMeta.remove() { + (parent?.node as? MutableMeta<*>)?.remove(name.asName()) +} + +fun > FXMetaNode.addValue(key: String){ + TODO() +} + +fun > FXMetaNode.addNode(key: String){ + TODO() +} + +fun FXMetaValue.set(value: Value?){ + TODO() +} \ No newline at end of file diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/MetaViewer.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/MetaViewer.kt new file mode 100644 index 00000000..3eddfb78 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/meta/MetaViewer.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package hep.dataforge.vis.fx.meta + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.seal +import hep.dataforge.vis.fx.dfIconView +import javafx.beans.property.SimpleStringProperty +import javafx.scene.control.TreeItem +import javafx.scene.control.TreeSortMode +import javafx.scene.control.TreeTableView +import tornadofx.* + +open class MetaViewer(val meta: Meta, title: String = "Meta viewer") : Fragment(title, dfIconView) { + override val root = borderpane { + center { + treetableview { + isShowRoot = false + root = TreeItem(FXMeta.root(meta.seal())) + populate { + when (val fxMeta = it.value) { + is FXMetaNode<*> -> { + fxMeta.children + } + is FXMetaValue -> null + } + } + root.isExpanded = true + sortMode = TreeSortMode.ALL_DESCENDANTS + columnResizePolicy = TreeTableView.CONSTRAINED_RESIZE_POLICY + column("Name", FXMeta::name) + column("Value"){ + when(val item = it.value.value){ + is FXMetaValue -> item.valueProperty.stringBinding{it?.string?: ""} + is FXMetaNode<*> -> SimpleStringProperty("[node]") + } + } + } + } + } +} \ No newline at end of file diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ColorValueChooser.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ColorValueChooser.kt new file mode 100644 index 00000000..e0e0f94a --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ColorValueChooser.kt @@ -0,0 +1,47 @@ +package hep.dataforge.vis.fx.values + +import hep.dataforge.meta.Meta +import hep.dataforge.values.Null +import hep.dataforge.values.Value +import hep.dataforge.values.asValue +import javafx.scene.control.ColorPicker +import javafx.scene.paint.Color +import org.slf4j.LoggerFactory +import tornadofx.* + +/** + * Created by darksnake on 01-May-17. + */ +class ColorValueChooser : ValueChooserBase() { + private fun ColorPicker.setColor(value: Value?) { + if (value != null && value != Null) { + try { + runLater { + this.value = Color.valueOf(value.string) + } + } catch (ex: Exception) { + LoggerFactory.getLogger(javaClass).warn("Invalid color field value: " + value.string) + } + } + } + + + override fun setDisplayValue(value: Value) { + node.setColor(value) + } + + override fun buildNode(): ColorPicker { + val node = ColorPicker() + node.styleClass.add("split-button") + node.maxWidth = java.lang.Double.MAX_VALUE + node.setColor(value) + node.setOnAction { _ -> value = node.value.toString().asValue() } + return node + } + + companion object: ValueChooser.Factory{ + override val name: String = "color" + + override fun invoke(meta: Meta): ValueChooser = ColorValueChooser() + } +} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ComboBoxValueChooser.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ComboBoxValueChooser.kt new file mode 100644 index 00000000..0f6460e0 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ComboBoxValueChooser.kt @@ -0,0 +1,58 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.values + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.value +import hep.dataforge.values.Value +import hep.dataforge.values.parseValue +import javafx.collections.FXCollections +import javafx.scene.control.ComboBox +import javafx.util.StringConverter +import java.util.* + +class ComboBoxValueChooser(val values: Collection? = null) : ValueChooserBase>() { + + // @Override + // protected void displayError(String error) { + // //TODO ControlsFX decorator here + // } + + private fun allowedValues(): Collection { + return values ?: descriptor?.allowedValues ?: Collections.emptyList(); + } + + override fun buildNode(): ComboBox { + val node = ComboBox(FXCollections.observableArrayList(allowedValues())) + node.maxWidth = java.lang.Double.MAX_VALUE + node.isEditable = false + node.selectionModel.select(currentValue()) + node.converter = object : StringConverter() { + override fun toString(value: Value?): String { + return value?.string ?: "" + } + + override fun fromString(string: String?): Value { + return (string ?: "").parseValue() + } + + } + this.valueProperty.bind(node.valueProperty()) + return node + } + + override fun setDisplayValue(value: Value) { + node.selectionModel.select(value) + } + + companion object : ValueChooser.Factory { + override val name: String = "combo" + + override fun invoke(meta: Meta): ValueChooser = ComboBoxValueChooser(meta["values"].value?.list) + } + +} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/TextValueChooser.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/TextValueChooser.kt new file mode 100644 index 00000000..b273b71f --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/TextValueChooser.kt @@ -0,0 +1,106 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.values + +import hep.dataforge.meta.Meta +import hep.dataforge.values.* +import javafx.beans.value.ObservableValue +import javafx.scene.control.TextField +import javafx.scene.input.KeyCode +import javafx.scene.input.KeyEvent +import tornadofx.* + +class TextValueChooser : ValueChooserBase() { + + private val displayText: String + get() = currentValue().let { + if (it.isNull()) { + "" + } else { + it.string + } + } + + + override fun buildNode(): TextField { + val node = TextField() + val defaultValue = currentValue() + node.text = displayText + node.style = String.format("-fx-text-fill: %s;", textColor(defaultValue)) + + // commit on enter + node.setOnKeyPressed { event: KeyEvent -> + if (event.code == KeyCode.ENTER) { + commit() + } + } + // restoring value on click outside + node.focusedProperty().addListener { _: ObservableValue, oldValue: Boolean, newValue: Boolean -> + if (oldValue && !newValue) { + node.text = displayText + } + } + + // changing text color while editing + node.textProperty().onChange { newValue -> + if (newValue != null) { + val value = newValue.parseValue() + if (!validate(value)) { + node.style = String.format("-fx-text-fill: %s;", "red") + } else { + node.style = String.format("-fx-text-fill: %s;", textColor(value)) + } + } + } + + return node + } + + private fun commit() { + val newValue = node.text.parseValue() + if (validate(newValue)) { + value = newValue + } else { + resetValue() + displayError("Value not allowed") + } + + } + + private fun textColor(item: Value): String { + return when (item.type) { + ValueType.BOOLEAN -> if (item.boolean) { + "blue" + } else { + "salmon" + } + ValueType.STRING -> "magenta" + else -> "black" + } + } + + private fun validate(value: Value): Boolean { + return descriptor?.isAllowedValue(value) ?: true + } + + // @Override + // protected void displayError(String error) { + // //TODO ControlsFX decorator here + // } + + override fun setDisplayValue(value: Value) { + node.text = if (value.isNull()) { + "" + } else { + value.string + } + } + + companion object : ValueChooser.Factory { + override val name: String = "text" + override fun invoke(meta: Meta): ValueChooser = TextValueChooser() + } +} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueCallback.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueCallback.kt new file mode 100644 index 00000000..c327ab22 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueCallback.kt @@ -0,0 +1,23 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.values + +import hep.dataforge.values.Value + + +/** + * @param success + * @param value Value after change + * @param message Message on unsuccessful change + */ +class ValueCallbackResponse(val success: Boolean, val value: Value, val message: String) + +/** + * A callback for some visual object trying to change some value + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +typealias ValueCallback = (Value) -> ValueCallbackResponse + diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooser.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooser.kt new file mode 100644 index 00000000..4635e87c --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooser.kt @@ -0,0 +1,116 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.values + +import hep.dataforge.context.Context +import hep.dataforge.context.Named +import hep.dataforge.descriptors.ValueDescriptor +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.provider.Type +import hep.dataforge.provider.provideByType +import hep.dataforge.values.Null +import hep.dataforge.values.Value +import javafx.beans.property.ObjectProperty +import javafx.beans.value.ObservableValue +import javafx.scene.Node +import tornadofx.* + +/** + * A value chooser object. Must have an empty constructor to be invoked by + * reflections. + * + * @author [Alexander Nozik](mailto:altavir@gmail.com) + */ +interface ValueChooser { + + /** + * Get or create a Node that could be later inserted into some parent + * object. + * + * @return + */ + val node: Node + + /** + * The descriptor property for this value. Could be null + * + * @return + */ + val descriptorProperty: ObjectProperty + var descriptor: ValueDescriptor? + + val valueProperty: ObjectProperty + var value: Value? + + + /** + * Set display value but do not notify listeners + * + * @param value + */ + fun setDisplayValue(value: Value) + + + fun setDisabled(disabled: Boolean) { + //TODO replace by property + } + + fun setCallback(callback: ValueCallback) + + @Type("hep.dataforge.vis.fx.valueChooserFactory") + interface Factory: Named { + operator fun invoke(meta: Meta = EmptyMeta): ValueChooser + } + + companion object { + + private fun findWidgetByType(context: Context, type: String): Factory? { + return when(type){ + TextValueChooser.name -> TextValueChooser + ColorValueChooser.name -> ColorValueChooser + ComboBoxValueChooser.name -> ComboBoxValueChooser + else-> context.provideByType(type)//Search for additional factories in the plugin + } + } + + private fun build(descriptor: ValueDescriptor?): ValueChooser { + return if (descriptor == null) { + TextValueChooser(); + } else { + //val types = descriptor.type + val chooser: ValueChooser = when { + descriptor.allowedValues.isNotEmpty() -> ComboBoxValueChooser() + descriptor.tags.contains("widget:color") -> ColorValueChooser() + else -> TextValueChooser() + } + chooser.descriptor = descriptor + chooser + } + } + + fun build( + value: ObservableValue, + descriptor: ValueDescriptor? = null, + setter: (Value) -> Unit + ): ValueChooser { + val chooser = build(descriptor) + chooser.setDisplayValue(value.value ?: Null) + value.onChange { + chooser.setDisplayValue(it ?: Null) + } + chooser.setCallback { result -> + if (descriptor?.isAllowedValue(result) != false) { + setter(result) + ValueCallbackResponse(true, result, "OK") + } else { + ValueCallbackResponse(false, value.value ?: Null, "Not allowed") + } + } + return chooser + } + } +} diff --git a/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooserBase.kt b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooserBase.kt new file mode 100644 index 00000000..a9e2fc01 --- /dev/null +++ b/dataforge-vis-fx/src/main/kotlin/hep/dataforge/vis/fx/values/ValueChooserBase.kt @@ -0,0 +1,70 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.vis.fx.values + +import hep.dataforge.descriptors.ValueDescriptor +import hep.dataforge.values.Null +import hep.dataforge.values.Value +import javafx.beans.property.SimpleObjectProperty +import javafx.scene.Node +import org.slf4j.LoggerFactory +import tornadofx.* + +/** + * ValueChooser boilerplate + * + * @author Alexander Nozik + */ +abstract class ValueChooserBase : ValueChooser { + + override val node by lazy { buildNode() } + final override val valueProperty = SimpleObjectProperty(Null) + final override val descriptorProperty = SimpleObjectProperty() + + override var descriptor: ValueDescriptor? by descriptorProperty + override var value: Value? by valueProperty + + fun resetValue() { + setDisplayValue(currentValue()) + } + + /** + * Current value or default value + * @return + */ + protected fun currentValue(): Value { + return value ?: descriptor?.default ?: Null + } + + /** + * True if builder node is successful + * + * @return + */ + protected abstract fun buildNode(): T + + /** + * Display validation error + * + * @param error + */ + protected fun displayError(error: String) { + LoggerFactory.getLogger(javaClass).error(error) + } + + override fun setCallback(callback: ValueCallback) { + valueProperty.onChange { newValue: Value? -> + val response = callback(newValue ?: Null) + if (response.value != valueProperty.get()) { + setDisplayValue(response.value) + } + + if (!response.success) { + displayError(response.message) + } + } + } +} diff --git a/dataforge-vis-fx/src/main/resources/img/df.png b/dataforge-vis-fx/src/main/resources/img/df.png new file mode 100644 index 0000000000000000000000000000000000000000..076e26a2ed849cbaae060d15adb6148a3b56a3c3 GIT binary patch literal 54732 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

s%ww{03OY|N=RqVm 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%0A!(<~w@))CgMB=C}tiVTR$`5(tbM}G`~7pY%g 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 literal 0 HcmV?d00001 diff --git a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/gdml/GDMLPlugin.kt b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/gdml/GDMLPlugin.kt index 6445755a..364f5637 100644 --- a/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/gdml/GDMLPlugin.kt +++ b/dataforge-vis-spatial-js/src/main/kotlin/hep/dataforge/vis/spatial/gdml/GDMLPlugin.kt @@ -7,6 +7,7 @@ import hep.dataforge.context.PluginTag import hep.dataforge.meta.Meta import hep.dataforge.names.toName import hep.dataforge.vis.spatial.ThreePlugin +import kotlin.reflect.KClass class GDMLPlugin : AbstractPlugin() { override val tag: PluginTag get() = GDMLPlugin.tag @@ -30,7 +31,7 @@ class GDMLPlugin : AbstractPlugin() { companion object : PluginFactory { override val tag = PluginTag("vis.gdml", "hep.dataforge") - override val type = GDMLPlugin::class + override val type: KClass = GDMLPlugin::class override fun invoke(meta: Meta) = GDMLPlugin() } } \ No newline at end of file diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Box.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Box.kt index b970927e..92d40a08 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Box.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Box.kt @@ -2,10 +2,8 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta -import hep.dataforge.vis.DisplayGroup -import hep.dataforge.vis.DisplayLeaf import hep.dataforge.vis.DisplayObject -import hep.dataforge.vis.double +import hep.dataforge.vis.DisplayObjectList class Box(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, meta) { var xSize by double(1.0) @@ -19,5 +17,5 @@ class Box(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, meta) } } -fun DisplayGroup.box(meta: Meta = EmptyMeta, action: Box.() -> Unit = {}) = +fun DisplayObjectList.box(meta: Meta = EmptyMeta, action: Box.() -> Unit = {}) = Box(this, meta).apply(action).also { addChild(it) } \ No newline at end of file diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Convex.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Convex.kt index 1f57be85..9b921e22 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Convex.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Convex.kt @@ -2,9 +2,8 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.* import hep.dataforge.names.toName -import hep.dataforge.vis.DisplayGroup -import hep.dataforge.vis.DisplayLeaf import hep.dataforge.vis.DisplayObject +import hep.dataforge.vis.DisplayObjectList class Convex(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, meta) { @@ -21,7 +20,7 @@ class Convex(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, met } } -fun DisplayGroup.convex(meta: Meta = EmptyMeta, action: ConvexBuilder.() -> Unit = {}) = +fun DisplayObjectList.convex(meta: Meta = EmptyMeta, action: ConvexBuilder.() -> Unit = {}) = ConvexBuilder().apply(action).build(this, meta).also { addChild(it) } class ConvexBuilder { diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Extruded.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Extruded.kt index 773191fc..6cdd04c8 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Extruded.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/Extruded.kt @@ -2,9 +2,8 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.* import hep.dataforge.names.toName -import hep.dataforge.vis.DisplayGroup -import hep.dataforge.vis.DisplayLeaf import hep.dataforge.vis.DisplayObject +import hep.dataforge.vis.DisplayObjectList class Extruded(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, meta) { @@ -22,5 +21,5 @@ class Extruded(parent: DisplayObject?, meta: Meta) : DisplayLeaf(parent, TYPE, m } -fun DisplayGroup.extrude(meta: Meta = EmptyMeta, action: Extruded.() -> Unit = {}) = +fun DisplayObjectList.extrude(meta: Meta = EmptyMeta, action: Extruded.() -> Unit = {}) = Extruded(this, meta).apply(action).also { addChild(it) } \ No newline at end of file diff --git a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt index 1f1ccd62..bd5d2e6d 100644 --- a/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt +++ b/dataforge-vis-spatial/src/commonMain/kotlin/hep/dataforge/vis/spatial/displayObject3D.kt @@ -3,17 +3,17 @@ package hep.dataforge.vis.spatial import hep.dataforge.meta.* import hep.dataforge.output.Output import hep.dataforge.vis.DisplayGroup -import hep.dataforge.vis.DisplayNode import hep.dataforge.vis.DisplayObject import hep.dataforge.vis.DisplayObject.Companion.DEFAULT_TYPE -import hep.dataforge.vis.get +import hep.dataforge.vis.DisplayObjectList +import hep.dataforge.vis.getProperty -fun DisplayGroup.group(meta: Meta = EmptyMeta, action: DisplayGroup.() -> Unit = {}) = - DisplayNode(this, DEFAULT_TYPE, meta).apply(action).also { addChild(it) } +fun DisplayObjectList.group(meta: Meta = EmptyMeta, action: DisplayObjectList.() -> Unit = {}): DisplayGroup = + DisplayObjectList(this, DEFAULT_TYPE, meta).apply(action).also { addChild(it) } -fun Output.render(meta: Meta = EmptyMeta, action: DisplayGroup.() -> Unit) = - render(DisplayNode(null, DEFAULT_TYPE, EmptyMeta).apply(action), meta) +fun Output.render(meta: Meta = EmptyMeta, action: DisplayObjectList.() -> Unit) = + render(DisplayObjectList(null, DEFAULT_TYPE, EmptyMeta).apply(action), meta) //TODO replace properties by containers? @@ -23,9 +23,9 @@ fun Output.render(meta: Meta = EmptyMeta, action: DisplayGroup.() * Visibility property. Inherited from parent */ var DisplayObject.visible - get() = this["visible"].boolean ?: true + get() = getProperty("visible").boolean ?: true set(value) { - properties.style["visible"] = value + properties["visible"] = value } // 3D Object position @@ -36,7 +36,7 @@ var DisplayObject.visible var DisplayObject.x get() = properties["pos.x"].number ?: 0.0 set(value) { - properties.style["pos.x"] = value + properties["pos.x"] = value } /** @@ -45,7 +45,7 @@ var DisplayObject.x var DisplayObject.y get() = properties["pos.y"].number ?: 0.0 set(value) { - properties.style["pos.y"] = value + properties["pos.y"] = value } /** @@ -54,7 +54,7 @@ var DisplayObject.y var DisplayObject.z get() = properties["pos.z"].number ?: 0.0 set(value) { - properties.style["pos.z"] = value + properties["pos.z"] = value } // 3D Object rotation @@ -65,7 +65,7 @@ var DisplayObject.z var DisplayObject.rotationX get() = properties["rotation.x"].number ?: 0.0 set(value) { - properties.style["rotation.x"] = value + properties["rotation.x"] = value } /** @@ -74,7 +74,7 @@ var DisplayObject.rotationX var DisplayObject.rotationY get() = properties["rotation.y"].number ?: 0.0 set(value) { - properties.style["rotation.y"] = value + properties["rotation.y"] = value } /** @@ -83,7 +83,7 @@ var DisplayObject.rotationY var DisplayObject.rotationZ get() = properties["rotation.z"].number ?: 0.0 set(value) { - properties.style["rotation.z"] = value + properties["rotation.z"] = value } enum class RotationOrder { @@ -101,7 +101,7 @@ enum class RotationOrder { var DisplayObject.rotationOrder: RotationOrder get() = properties["rotation.order"].enum() ?: RotationOrder.XYZ set(value) { - properties.style["rotation.order"] = value + properties["rotation.order"] = value } // 3D object scale @@ -112,7 +112,7 @@ var DisplayObject.rotationOrder: RotationOrder var DisplayObject.scaleX get() = properties["scale.x"].number ?: 1.0 set(value) { - properties.style["scale.x"] = value + properties["scale.x"] = value } /** @@ -121,7 +121,7 @@ var DisplayObject.scaleX var DisplayObject.scaleY get() = properties["scale.y"].number ?: 1.0 set(value) { - properties.style["scale.y"] = value + properties["scale.y"] = value } /** @@ -130,15 +130,15 @@ var DisplayObject.scaleY var DisplayObject.scaleZ get() = properties["scale.z"].number ?: 1.0 set(value) { - properties.style["scale.z"] = value + properties["scale.z"] = value } -fun DisplayObject.color(rgb: Int){ - this.properties.style["color"] = rgb +fun DisplayObject.color(rgb: Int) { + this.properties["color"] = rgb } -fun DisplayObject.color(meta: Meta){ - this.properties.style["color"] = meta +fun DisplayObject.color(meta: Meta) { + this.properties["color"] = meta } fun DisplayObject.color(r: Int, g: Int, b: Int) = color(buildMeta { diff --git a/dataforge-vis-spatial/src/commonTest/kotlin/hep/dataforge/vis/spatial/ConvexTest.kt b/dataforge-vis-spatial/src/commonTest/kotlin/hep/dataforge/vis/spatial/ConvexTest.kt index 1b83c1e0..7d8fbb58 100644 --- a/dataforge-vis-spatial/src/commonTest/kotlin/hep/dataforge/vis/spatial/ConvexTest.kt +++ b/dataforge-vis-spatial/src/commonTest/kotlin/hep/dataforge/vis/spatial/ConvexTest.kt @@ -1,17 +1,14 @@ package hep.dataforge.vis.spatial -import hep.dataforge.meta.get -import hep.dataforge.meta.getAll -import hep.dataforge.meta.node import hep.dataforge.names.toName -import hep.dataforge.vis.DisplayNode +import hep.dataforge.vis.DisplayObjectList import kotlin.test.Test import kotlin.test.assertEquals class ConvexTest { @Test fun testConvexBuilder() { - val group = DisplayNode().apply { + val group = DisplayObjectList().apply { convex { point(50, 50, -50) point(50, -50, -50) diff --git a/settings.gradle.kts b/settings.gradle.kts index 95d02740..3cb6cf55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,15 +22,16 @@ rootProject.name = "dataforge-vis" include( ":dataforge-vis-common", + ":dataforge-vis-fx", ":dataforge-vis-spatial", ":dataforge-vis-spatial-fx", ":dataforge-vis-spatial-js" ) -if(file("../dataforge-core").exists()) { - includeBuild("../dataforge-core"){ - dependencySubstitution { - substitute(module("hep.dataforge:dataforge-output")).with(project(":dataforge-output")) - } - } -} \ No newline at end of file +//if(file("../dataforge-core").exists()) { +// includeBuild("../dataforge-core"){ +// dependencySubstitution { +// substitute(module("hep.dataforge:dataforge-output")).with(project(":dataforge-output")) +// } +// } +//} \ No newline at end of file