From 7e4b1dedfd19279462ec207091eb7da0ea823c2f Mon Sep 17 00:00:00 2001 From: Almas Baim Date: Fri, 22 Mar 2024 13:30:19 +0000 Subject: [PATCH] feat: the initial framework for PropertyMapView is implemented, closes #704 --- .../sandbox/view/PropertyMapViewSample.java | 76 +++++------ .../com/almasb/fxgl/ui/PropertyMapView.java | 125 ------------------ .../com/almasb/fxgl/ui/UIFactoryService.java | 3 + .../property/Point2DPropertyViewFactory.java | 85 ++++++++++++ .../PropertyViewFactory.java} | 17 ++- ...ener.java => Vec2PropertyViewFactory.java} | 6 +- .../fxgl/ui/FXGLUIFactoryServiceProvider.kt | 56 ++++++-- .../almasb/fxgl/ui/property/PropertyViews.kt | 35 ++++- .../almasb/fxgl/app/MockUIFactoryService.kt | 5 + 9 files changed, 224 insertions(+), 184 deletions(-) delete mode 100644 fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyMapView.java create mode 100644 fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Point2DPropertyViewFactory.java rename fxgl-scene/src/main/java/com/almasb/fxgl/ui/{PropertyViewChangeListener.java => property/PropertyViewFactory.java} (59%) rename fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/{Vec2PropertyViewChangeListener.java => Vec2PropertyViewFactory.java} (93%) diff --git a/fxgl-samples/src/main/java/sandbox/view/PropertyMapViewSample.java b/fxgl-samples/src/main/java/sandbox/view/PropertyMapViewSample.java index dd9b3bcb21..ee800e9274 100644 --- a/fxgl-samples/src/main/java/sandbox/view/PropertyMapViewSample.java +++ b/fxgl-samples/src/main/java/sandbox/view/PropertyMapViewSample.java @@ -9,72 +9,72 @@ import com.almasb.fxgl.app.GameApplication; import com.almasb.fxgl.app.GameSettings; import com.almasb.fxgl.core.collection.PropertyMap; -import com.almasb.fxgl.ui.PropertyMapView; +import com.almasb.fxgl.core.math.Vec2; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.input.KeyCode; +import javafx.scene.paint.Color; import static com.almasb.fxgl.dsl.FXGL.*; public class PropertyMapViewSample extends GameApplication { + + private enum TestEnum { + ONE,TWO,THREE + } - PropertyMap map = new PropertyMap(); + private PropertyMap map = new PropertyMap(); @Override - protected void initSettings(GameSettings settings) { - settings.setWidth(800); - settings.setHeight(600); - settings.setTitle("PropertyMapViewSample"); - settings.setVersion("0.1"); - } - + protected void initSettings(GameSettings settings) { } + @Override - protected void initGame() { - entityBuilder() - .view("background.png") - .buildAndAttach(); - map.setValue("Music Volume", 10); - map.setValue("Camera Distance",120); - map.setValue("Initial Health", 100.0); - map.setValue("Initial Position", new Point2D(0, 0)); - map.setValue("Enum Test", TestEnum.ONE); - map.setValue("Color Blind", false); - map.setValue("Player Name", "Jamie"); - - Node display = new PropertyMapView(map); - display.setLayoutX(getAppWidth() / 2); - getGameScene().addUINode(display); - + protected void initInput() { onKeyDown(KeyCode.G, "map", () -> { for (String key : map.keys()) { - debug(key + " - " + map.getValue(key).toString()); + System.out.println(key + " - " + map.getValue(key).toString()); } }); - onKeyDown(KeyCode.DIGIT1, "1", () -> { + onKeyDown(KeyCode.DIGIT1, () -> { map.setValue("Enum Test", TestEnum.ONE); map.setValue("Color Blind", true); map.setValue("Initial Health", 1.0); map.setValue("Player Name", "Jamie"); + map.setValue("Main color", Color.ORANGE); + map.setValue("Initial Position Point2D", new Point2D(5, 1)); + map.setValue("Initial Position Vec2", new Vec2(5, 1)); }); - onKeyDown(KeyCode.DIGIT2, "2", () -> { + onKeyDown(KeyCode.DIGIT2, () -> { map.setValue("Enum Test", TestEnum.TWO); map.setValue("Color Blind", false); map.setValue("Initial Health", 100.0); map.setValue("Player Name", "Dave"); - }); - onKeyDown(KeyCode.DIGIT3, "3", () -> { - map.setValue("Enum Test", TestEnum.THREE); - map.setValue("Color Blind", true); - map.setValue("Initial Health", 123.45); - map.setValue("Player Name", "Christ"); + map.setValue("Main color", Color.RED); + map.setValue("Initial Position Point2D", new Point2D(-6, 2)); + map.setValue("Initial Position Vec2", new Vec2(-6, 2)); }); } + + @Override + protected void initGame() { + getGameScene().setBackgroundColor(Color.DARKGRAY); + + map.setValue("Music Volume", 10); + map.setValue("Camera Distance", 120); + map.setValue("Initial Health", 100.0); + map.setValue("Initial Position Point2D", new Point2D(0, 0)); + map.setValue("Initial Position Vec2", new Vec2(0, 0)); + map.setValue("Enum Test", TestEnum.ONE); + map.setValue("Color Blind", false); + map.setValue("Player Name", "Jamie"); + map.setValue("Main color", Color.BLUE); - public static void main(String[] args) { - launch(args); + Node view = getUIFactoryService().newPropertyMapView(map); + + addUINode(view, 100, 100); } - private enum TestEnum { - ONE,TWO,THREE + public static void main(String[] args) { + launch(args); } } diff --git a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyMapView.java b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyMapView.java deleted file mode 100644 index d55cfbb8db..0000000000 --- a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyMapView.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * FXGL - JavaFX Game Library. The MIT License (MIT). - * Copyright (c) AlmasB (almaslvl@gmail.com). - * See LICENSE for details. - */ - -package com.almasb.fxgl.ui; - -import com.almasb.fxgl.core.collection.PropertyMap; -import com.almasb.fxgl.core.math.Vec2; -import com.almasb.fxgl.ui.property.Vec2PropertyViewChangeListener; -import javafx.beans.property.*; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.control.CheckBox; -import javafx.scene.control.ChoiceBox; -import javafx.scene.control.TextField; -import javafx.scene.layout.*; -import javafx.scene.paint.Color; -import javafx.scene.text.Text; -import javafx.util.converter.DefaultStringConverter; -import javafx.util.converter.DoubleStringConverter; -import javafx.util.converter.IntegerStringConverter; - -import java.util.HashMap; -import java.util.Map; - -/** - * @author Charly Zhu (charlyzhu@hotmail.com) - */ -public class PropertyMapView extends Parent { - - public static final Map, PropertyViewChangeListener> converters = new HashMap<>(); - - static { - addViewConverter(Vec2.class, new Vec2PropertyViewChangeListener()); - } - - public static void addViewConverter(Class type, PropertyViewChangeListener converter) { - converters.put(type, converter); - } - - public PropertyMapView(PropertyMap map) { - VBox rootBox = new VBox(); - rootBox.setBackground(new Background(new BackgroundFill(Color.gray(1.0), CornerRadii.EMPTY, Insets.EMPTY))); - - for (String key : map.keys()) { - HBox propertyBox = new HBox(); - - Text name = new Text(key); - name.setWrappingWidth(150); - Node value = makeView(map.getValueObservable(key)); - - propertyBox.getChildren().add(name); - propertyBox.getChildren().add(value); - rootBox.getChildren().add(propertyBox); - } - - getChildren().add(rootBox); - } - - /** - * Makes a check box when it is a boolean value and text field when it is string etc. - */ - @SuppressWarnings("unchecked") - private Node makeView(Object value) { - if (value instanceof BooleanProperty) { - CheckBox box = new CheckBox(); - box.selectedProperty().bindBidirectional((BooleanProperty) value); - return box; - } else if (value instanceof ObjectProperty) { - ObjectProperty property = (ObjectProperty) value; - - // If object is an enum. - if (property.get().getClass().isEnum()) - return makeEnumView((ObjectProperty>) property); - - // If object is something else. - if (converters.containsKey(property.get().getClass())) { - PropertyViewChangeListener converter = converters.get(property.get().getClass()); - - return converter.makeViewInternal(property); - - } else { - // If object does not have a converter. - - Text text = new Text(); - text.textProperty().bind(property.asString()); - return text; - } - - } else { - TextField textField = new TextField(); - textField.setPrefWidth(150); - - if (value instanceof StringProperty) - textField.textProperty().bindBidirectional((StringProperty) value, new DefaultStringConverter()); - if (value instanceof IntegerProperty) - textField.textProperty().bindBidirectional((Property) value, new IntegerStringConverter()); - if (value instanceof DoubleProperty) - textField.textProperty().bindBidirectional((Property) value, new DoubleStringConverter()); - - return textField; - } - } - - private Node makeEnumView(ObjectProperty> enumProperty) { - Enum enumValue = enumProperty.get(); - - ObservableList> list = FXCollections.observableArrayList(); - for (Object anEnum : enumValue.getDeclaringClass().getEnumConstants()) { - list.add((Enum) anEnum); - } - - ChoiceBox> view = new ChoiceBox<>(); - view.setItems(list); - view.setValue(enumValue); - view.valueProperty().bindBidirectional(enumProperty); - - return view; - } -} diff --git a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/UIFactoryService.java b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/UIFactoryService.java index 9b31d6ce52..c97295c121 100644 --- a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/UIFactoryService.java +++ b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/UIFactoryService.java @@ -7,6 +7,7 @@ package com.almasb.fxgl.ui; import com.almasb.fxgl.core.EngineService; +import com.almasb.fxgl.core.collection.PropertyMap; import javafx.beans.binding.StringBinding; import javafx.beans.binding.StringExpression; import javafx.collections.ObservableList; @@ -67,4 +68,6 @@ public abstract class UIFactoryService extends EngineService { public abstract FXGLTextFlow newTextFlow(); public abstract Node newPropertyView(String propertyName, Object property); + + public abstract Node newPropertyMapView(PropertyMap map); } diff --git a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Point2DPropertyViewFactory.java b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Point2DPropertyViewFactory.java new file mode 100644 index 0000000000..222cae0916 --- /dev/null +++ b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Point2DPropertyViewFactory.java @@ -0,0 +1,85 @@ +/* + * FXGL - JavaFX Game Library. The MIT License (MIT). + * Copyright (c) AlmasB (almaslvl@gmail.com). + * See LICENSE for details. + */ + +package com.almasb.fxgl.ui.property; + +import javafx.beans.property.ObjectProperty; +import javafx.geometry.Point2D; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; + +/** + * // TODO: read-only version? + * + * @author Almas Baimagambetov (almaslvl@gmail.com) + */ +public class Point2DPropertyViewFactory implements PropertyViewFactory { + + private boolean ignoreChangeView = false; + private boolean ignoreChangeProperty = false; + + @Override + public HBox makeView(ObjectProperty value) { + var fieldX = new TextField(); + var fieldY = new TextField(); + HBox view = new HBox(fieldX, fieldY); + + value.addListener((obs, o, newValue) -> { + if (ignoreChangeProperty) + return; + + onPropertyChanged(value, view); + }); + + fieldX.textProperty().addListener((obs, o, x) -> { + if (ignoreChangeView) + return; + + onViewChanged(value, view); + }); + + fieldY.textProperty().addListener((obs, o, y) -> { + if (ignoreChangeView) + return; + + onViewChanged(value, view); + }); + + onPropertyChanged(value, view); + + return view; + } + + @Override + public void onPropertyChanged(ObjectProperty value, HBox view) { + var fieldX = (TextField) view.getChildren().get(0); + var fieldY = (TextField) view.getChildren().get(1); + + ignoreChangeView = true; + + fieldX.setText(Double.toString(value.getValue().getX())); + fieldY.setText(Double.toString(value.getValue().getY())); + + ignoreChangeView = false; + } + + @Override + public void onViewChanged(ObjectProperty value, HBox view) { + var fieldX = (TextField) view.getChildren().get(0); + var fieldY = (TextField) view.getChildren().get(1); + + ignoreChangeProperty = true; + + var newPoint = new Point2D( + Double.parseDouble(fieldX.getText()), + Double.parseDouble(fieldY.getText()) + ); + + value.setValue(newPoint); + + ignoreChangeProperty = false; + } +} diff --git a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyViewChangeListener.java b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/PropertyViewFactory.java similarity index 59% rename from fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyViewChangeListener.java rename to fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/PropertyViewFactory.java index 15789a5c4b..d2f53de6dd 100644 --- a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/PropertyViewChangeListener.java +++ b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/PropertyViewFactory.java @@ -4,7 +4,7 @@ * See LICENSE for details. */ -package com.almasb.fxgl.ui; +package com.almasb.fxgl.ui.property; import javafx.beans.property.ObjectProperty; import javafx.scene.Node; @@ -12,15 +12,24 @@ /** * @author Almas Baimagambetov (almaslvl@gmail.com) */ -public interface PropertyViewChangeListener { +public interface PropertyViewFactory { default V makeViewInternal(ObjectProperty v) { return makeView((ObjectProperty) v); } + /** + * @return the visual representation of the property [value] + */ V makeView(ObjectProperty value); - + + /** + * Called when the property has changed, so that the view can be updated. + */ void onPropertyChanged(ObjectProperty value, V view); - + + /** + * Called when the view has changed, so that the property can be updated. + */ void onViewChanged(ObjectProperty value, V view); } diff --git a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewChangeListener.java b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewFactory.java similarity index 93% rename from fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewChangeListener.java rename to fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewFactory.java index ccb1d74bcf..770cefc65f 100644 --- a/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewChangeListener.java +++ b/fxgl-scene/src/main/java/com/almasb/fxgl/ui/property/Vec2PropertyViewFactory.java @@ -8,15 +8,17 @@ import com.almasb.fxgl.core.collection.UpdatableObjectProperty; import com.almasb.fxgl.core.math.Vec2; -import com.almasb.fxgl.ui.PropertyViewChangeListener; import javafx.beans.property.ObjectProperty; import javafx.scene.control.TextField; import javafx.scene.layout.HBox; /** + * // TODO: read-only version? + * // TODO: empty String check when view is updated + * * @author Almas Baimagambetov (almaslvl@gmail.com) */ -public class Vec2PropertyViewChangeListener implements PropertyViewChangeListener { +public class Vec2PropertyViewFactory implements PropertyViewFactory { private boolean ignoreChangeView = false; private boolean ignoreChangeProperty = false; diff --git a/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/FXGLUIFactoryServiceProvider.kt b/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/FXGLUIFactoryServiceProvider.kt index 70d1881e3a..2a030c97f7 100644 --- a/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/FXGLUIFactoryServiceProvider.kt +++ b/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/FXGLUIFactoryServiceProvider.kt @@ -6,19 +6,21 @@ package com.almasb.fxgl.ui +import com.almasb.fxgl.core.collection.PropertyMap +import com.almasb.fxgl.core.math.Vec2 import com.almasb.fxgl.logging.Logger import com.almasb.fxgl.ui.FontType.UI -import com.almasb.fxgl.ui.property.BooleanPropertyView -import com.almasb.fxgl.ui.property.DoublePropertyView -import com.almasb.fxgl.ui.property.IntPropertyView -import com.almasb.fxgl.ui.property.StringPropertyView +import com.almasb.fxgl.ui.property.* import javafx.beans.binding.* import javafx.beans.property.* import javafx.collections.ObservableList +import javafx.geometry.Point2D +import javafx.geometry.Pos import javafx.scene.Node import javafx.scene.control.* import javafx.scene.layout.HBox import javafx.scene.layout.StackPane +import javafx.scene.layout.VBox import javafx.scene.paint.Color import javafx.scene.text.Font import javafx.scene.text.Text @@ -34,11 +36,16 @@ class FXGLUIFactoryServiceProvider : UIFactoryService() { private val log = Logger.get(javaClass) private val fontFactories = hashMapOf>() + private val propertyViewFactories = hashMapOf, PropertyViewFactory<*, *>>() init { FontType.values().forEach { fontType -> fontFactories[fontType] = SimpleObjectProperty(FontFactory(Font.font(18.0))) } + + propertyViewFactories[Vec2::class.java] = Vec2PropertyViewFactory() + propertyViewFactories[Color::class.java] = ColorPropertyViewFactory() + propertyViewFactories[Point2D::class.java] = Point2DPropertyViewFactory() } override fun registerFontFactory(type: FontType, fontFactory: FontFactory) { @@ -137,7 +144,10 @@ class FXGLUIFactoryServiceProvider : UIFactoryService() { } override fun newPropertyView(propertyName: String, property: Any): Node { - val text = Text(propertyName).also { it.fill = Color.WHITE } + val text = Text(propertyName).also { + it.fill = Color.WHITE + it.wrappingWidth = 100.0 + } val view: Node = when (property) { is ReadOnlyDoubleProperty -> DoublePropertyView(property) @@ -153,22 +163,44 @@ class FXGLUIFactoryServiceProvider : UIFactoryService() { is StringBinding -> StringPropertyView(property) is ObjectProperty<*> -> { - - if (property.get().javaClass in PropertyMapView.converters) { - val converter = PropertyMapView.converters[property.get().javaClass]!! + if (property.get().javaClass in propertyViewFactories) { + val converter = propertyViewFactories[property.get().javaClass]!! converter.makeViewInternal(property) } else { - // TODO: - Text("Not supported: ${property.get().javaClass}").also { it.fill = Color.WHITE } + if (property.get().javaClass.isEnum) { + EnumPropertyView(property as ObjectProperty>) + } else { + Text("Not supported ObjectProperty: ${property.get().javaClass}").also { it.fill = Color.WHITE } + } } } - else -> Text("Not supported: $property").also { it.fill = Color.WHITE } + else -> Text("Not supported property type: $property").also { it.fill = Color.WHITE } + } + + return HBox(10.0, + StackPane(text).also { + it.prefWidth = 100.0 + it.alignment = Pos.CENTER_LEFT + }, + StackPane(view).also { + it.prefWidth = 80.0 + } + ) + } + + override fun newPropertyMapView(map: PropertyMap): Node { + val vbox = VBox(5.0) + + map.forEachObservable { key, property -> + val node = newPropertyView(key, property) + + vbox.children += node } - return HBox(10.0, StackPane(text).also { it.prefWidth = 100.0 }, StackPane(view).also { it.prefWidth = 80.0 }) + return vbox } private fun fontProperty(type: FontType, fontSize: Double) = diff --git a/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/property/PropertyViews.kt b/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/property/PropertyViews.kt index e03edccfe0..9e42acedd5 100644 --- a/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/property/PropertyViews.kt +++ b/fxgl-scene/src/main/kotlin/com/almasb/fxgl/ui/property/PropertyViews.kt @@ -18,9 +18,14 @@ import javafx.beans.value.ObservableBooleanValue import javafx.beans.value.ObservableDoubleValue import javafx.beans.value.ObservableIntegerValue import javafx.beans.value.ObservableStringValue +import javafx.collections.FXCollections +import javafx.geometry.Point2D import javafx.scene.Parent import javafx.scene.control.CheckBox +import javafx.scene.control.ChoiceBox +import javafx.scene.control.ColorPicker import javafx.scene.control.TextField +import javafx.scene.paint.Color import javafx.util.converter.DoubleStringConverter import javafx.util.converter.IntegerStringConverter @@ -81,11 +86,35 @@ class StringPropertyView(property: ObservableStringValue) : TextField() { } } -class Vec2PropertyView(property: ObjectProperty) : Parent() { +class EnumPropertyView(enumProperty: ObjectProperty>) : ChoiceBox>() { init { - children += Vec2PropertyViewChangeListener().makeViewInternal(property) + val enumValue = enumProperty.get() - // TODO: handle read-only version + val list = FXCollections.observableArrayList>() + for (anEnum in enumValue.javaClass.getEnumConstants()) { + list.add(anEnum as Enum<*>) + } + + setItems(list) + setValue(enumValue) + valueProperty().bindBidirectional(enumProperty) + + // TODO: read only version } } + +class ColorPropertyViewFactory : PropertyViewFactory { + override fun makeView(value: ObjectProperty): ColorPicker { + val picker = ColorPicker() + + // TODO: handle read-only version + picker.valueProperty().bindBidirectional(value) + + return picker + } + + override fun onPropertyChanged(value: ObjectProperty, view: ColorPicker) { } + + override fun onViewChanged(value: ObjectProperty, view: ColorPicker) { } +} \ No newline at end of file diff --git a/fxgl/src/test/kotlin/com/almasb/fxgl/app/MockUIFactoryService.kt b/fxgl/src/test/kotlin/com/almasb/fxgl/app/MockUIFactoryService.kt index 4a1b8249f9..8aec019bc3 100644 --- a/fxgl/src/test/kotlin/com/almasb/fxgl/app/MockUIFactoryService.kt +++ b/fxgl/src/test/kotlin/com/almasb/fxgl/app/MockUIFactoryService.kt @@ -6,6 +6,7 @@ package com.almasb.fxgl.app +import com.almasb.fxgl.core.collection.PropertyMap import com.almasb.fxgl.ui.* import javafx.beans.binding.StringBinding import javafx.beans.binding.StringExpression @@ -102,4 +103,8 @@ object MockUIFactoryService : UIFactoryService() { override fun newPropertyView(propertyName: String?, property: Any?): Node { return Button() } + + override fun newPropertyMapView(map: PropertyMap?): Node { + return Button() + } } \ No newline at end of file