T bindNodeToAction(T node, Action action, boolea
public static void bindWritableActionToAction(WritableAction writableAction, Action action) {
writableAction.writableTextProperty().bind(action.textProperty());
- writableAction.writableGraphicProperty().bind(action.graphicProperty());
+ writableAction.writableGraphicFactoryProperty().bind(action.graphicFactoryProperty());
writableAction.writableDisabledProperty().bind(action.disabledProperty());
writableAction.writableVisibleProperty().bind(action.visibleProperty());
}
- public static P bindChildrenToVisibleActions(P parent, Collection actions, Converter nodeFactory) {
+
+ public static Unregisterable bindChildrenToVisibleActions(P parent, Collection actions, Converter nodeFactory) {
ActionGroup actionGroup = new ActionGroupBuilder().setActions(actions).build();
return bindChildrenToActionGroup(parent, actionGroup, nodeFactory);
}
- public static P bindChildrenToActionGroup(P parent, ActionGroup actionGroup, Converter nodeFactory) {
- bindChildrenToActionGroup(parent.getChildren(), actionGroup, nodeFactory);
- return parent;
+ public static Unregisterable bindChildrenToActionGroup(P parent, ActionGroup actionGroup, Converter nodeFactory) {
+ return bindChildrenToActionGroup(parent.getChildren(), actionGroup, nodeFactory);
}
- public static ObservableList bindChildrenToActionGroup(ObservableList children, ActionGroup actionGroup, Converter nodeFactory) {
- ObservableLists.bindConverted(children, actionGroup.getVisibleActions(), nodeFactory);
- return children;
+ public static Unregisterable bindChildrenToActionGroup(ObservableList children, ActionGroup actionGroup, Converter nodeFactory) {
+ return ObservableLists.bindConverted(children, actionGroup.getVisibleActions(), nodeFactory);
}
}
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java
index 9575ff494..904e1eb90 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionBuilder.java
@@ -1,17 +1,20 @@
package dev.webfx.stack.ui.action;
+import dev.webfx.kit.util.properties.FXProperties;
+import dev.webfx.stack.i18n.I18n;
+import dev.webfx.stack.ui.json.JsonImageView;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
-import dev.webfx.stack.i18n.I18n;
-import dev.webfx.stack.ui.json.JsonImageView;
+
+import java.util.function.Supplier;
/**
* @author Bruno Salmon
@@ -24,8 +27,8 @@ public class ActionBuilder {
private String text;
private Object i18nKey;
- private ObservableObjectValue graphicProperty;
- private Node graphic;
+ private ObservableValue> graphicFactoryProperty;
+ private Supplier graphicFactory;
private Object graphicUrlOrJson;
private ObservableBooleanValue disabledProperty;
@@ -39,6 +42,8 @@ public class ActionBuilder {
private EventHandler actionHandler;
+ private Object userData;
+
private ActionBuilderRegistry registry;
public ActionBuilder() {
@@ -48,6 +53,18 @@ public ActionBuilder(Object actionKey) {
this.actionKey = actionKey;
}
+ public ActionBuilder(Action action) {
+ setTextProperty(action.textProperty());
+ setText(action.getText());
+ setGraphicFactoryProperty(action.graphicFactoryProperty());
+ setGraphicFactory(action.getGraphicFactory());
+ setDisabledProperty(action.disabledProperty());
+ setVisibleProperty(action.visibleProperty());
+ setActionHandler(action);
+ setUserData(action.getUserData());
+ //setHiddenWhenDisabled(visibleProperty == disabledProperty || visibleProperty instanceof BooleanBinding && ((BooleanBinding) visibleProperty).getDependencies().contains(disabledProperty));
+ }
+
public Object getActionKey() {
return actionKey;
}
@@ -84,21 +101,21 @@ public ActionBuilder setI18nKey(Object i18nKey) {
return this;
}
- public ObservableObjectValue getGraphicProperty() {
- return graphicProperty;
+ public ObservableValue> getGraphicFactoryProperty() {
+ return graphicFactoryProperty;
}
- public ActionBuilder setGraphicProperty(ObservableObjectValue graphicProperty) {
- this.graphicProperty = graphicProperty;
+ public ActionBuilder setGraphicFactoryProperty(ObservableValue> graphicFactoryProperty) {
+ this.graphicFactoryProperty = graphicFactoryProperty;
return this;
}
- public Node getGraphic() {
- return graphic;
+ public Supplier getGraphicFactory() {
+ return graphicFactory;
}
- public ActionBuilder setGraphic(Node graphic) {
- this.graphic = graphic;
+ public ActionBuilder setGraphicFactory(Supplier graphicFactory) {
+ this.graphicFactory = graphicFactory;
return this;
}
@@ -169,6 +186,15 @@ public ActionBuilder setActionHandler(Runnable actionHandler) {
return setActionHandler(e -> actionHandler.run());
}
+ public Object getUserData() {
+ return userData;
+ }
+
+ public ActionBuilder setUserData(Object userData) {
+ this.userData = userData;
+ return this;
+ }
+
public ActionBuilder setRegistry(ActionBuilderRegistry registry) {
this.registry = registry;
return this;
@@ -184,8 +210,8 @@ public ActionBuilder duplicate() {
.setTextProperty(textProperty)
.setText(text)
.setI18nKey(i18nKey)
- .setGraphicProperty(graphicProperty)
- .setGraphic(graphic)
+ .setGraphicFactoryProperty(graphicFactoryProperty)
+ .setGraphicFactory(graphicFactory)
.setGraphicUrlOrJson(graphicUrlOrJson)
.setDisabledProperty(disabledProperty)
.setVisibleProperty(visibleProperty)
@@ -209,7 +235,9 @@ public ActionBuilder removeText() {
public Action build() {
completePropertiesForBuild();
- return Action.create(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler);
+ Action action = Action.create(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler);
+ action.setUserData(userData);
+ return action;
}
void completePropertiesForBuild() {
@@ -229,13 +257,14 @@ private void completeTextProperty() {
}
private void completeGraphicProperty() {
- if (graphicProperty == null) {
- if (graphic == null && graphicUrlOrJson != null)
- graphic = JsonImageView.createImageView(graphicUrlOrJson);
- if (graphic != null || i18nKey == null)
- graphicProperty = new SimpleObjectProperty<>(graphic);
+ if (graphicFactoryProperty == null) {
+ if (graphicFactory == null && graphicUrlOrJson != null)
+ graphicFactory = () -> JsonImageView.createImageView(graphicUrlOrJson);
+ if (graphicFactory != null || i18nKey == null)
+ graphicFactoryProperty = new SimpleObjectProperty<>(graphicFactory);
else
- graphicProperty = I18n.i18nGraphicProperty(i18nKey);
+ graphicFactoryProperty = FXProperties.compute(I18n.dictionaryProperty(), dictionary ->
+ () -> I18n.getI18nGraphic(i18nKey));
}
}
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java
index 438acab5a..71838eb4a 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroup.java
@@ -2,14 +2,15 @@
import dev.webfx.stack.ui.action.impl.ActionGroupImpl;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import java.util.Collection;
+import java.util.function.Supplier;
/**
* @author Bruno Salmon
@@ -22,8 +23,8 @@ public interface ActionGroup extends Action {
boolean hasSeparators();
- static ActionGroup create(Collection actions, ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) {
- return new ActionGroupImpl(actions, textProperty, graphicProperty, disabledProperty, visibleProperty, hasSeparators, actionHandler);
+ static ActionGroup create(Collection actions, ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) {
+ return new ActionGroupImpl(actions, textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, hasSeparators, actionHandler);
}
}
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java
index a0185e70a..ea2fd6bfa 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/ActionGroupBuilder.java
@@ -1,14 +1,15 @@
package dev.webfx.stack.ui.action;
+import dev.webfx.platform.util.collection.Collections;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
-import dev.webfx.platform.util.collection.Collections;
import java.util.Collection;
+import java.util.function.Supplier;
/**
* @author Bruno Salmon
@@ -43,7 +44,7 @@ public ActionGroupBuilder setHasSeparators(boolean hasSeparators) {
@Override
public ActionGroup build() {
completePropertiesForBuild();
- return ActionGroup.create(actions, getTextProperty(), getGraphicProperty(), getDisabledProperty(), getVisibleProperty(), hasSeparators, getActionHandler());
+ return ActionGroup.create(actions, getTextProperty(), getGraphicFactoryProperty(), getDisabledProperty(), getVisibleProperty(), hasSeparators, getActionHandler());
}
// --- Overriding fluent API methods to return ActionGroupBuilder instead of ActionBuilder ---
@@ -69,13 +70,13 @@ public ActionGroupBuilder setI18nKey(Object i18nKey) {
}
@Override
- public ActionGroupBuilder setGraphicProperty(ObservableObjectValue graphicProperty) {
- return (ActionGroupBuilder) super.setGraphicProperty(graphicProperty);
+ public ActionGroupBuilder setGraphicFactoryProperty(ObservableValue> graphicFactoryProperty) {
+ return (ActionGroupBuilder) super.setGraphicFactoryProperty(graphicFactoryProperty);
}
@Override
- public ActionGroupBuilder setGraphic(Node graphic) {
- return (ActionGroupBuilder) super.setGraphic(graphic);
+ public ActionGroupBuilder setGraphicFactory(Supplier graphicFactory) {
+ return (ActionGroupBuilder) super.setGraphicFactory(graphicFactory);
}
@Override
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java
index e331342e5..d37f5620e 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ActionGroupImpl.java
@@ -1,22 +1,23 @@
package dev.webfx.stack.ui.action.impl;
+import dev.webfx.kit.util.properties.FXProperties;
+import dev.webfx.platform.util.collection.Collections;
+import dev.webfx.stack.ui.action.Action;
+import dev.webfx.stack.ui.action.ActionGroup;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
-import dev.webfx.stack.ui.action.Action;
-import dev.webfx.stack.ui.action.ActionGroup;
-import dev.webfx.kit.util.properties.FXProperties;
-import dev.webfx.platform.util.collection.Collections;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.function.Supplier;
/**
* @author Bruno Salmon
@@ -27,8 +28,8 @@ public final class ActionGroupImpl extends ReadOnlyAction implements ActionGroup
private final ObservableList visibleActions = FXCollections.observableArrayList();
private final boolean hasSeparators;
- public ActionGroupImpl(Collection actions, ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) {
- super(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler);
+ public ActionGroupImpl(Collection actions, ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, boolean hasSeparators, EventHandler actionHandler) {
+ super(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler);
this.actions = actions;
this.hasSeparators = hasSeparators;
FXProperties.runNowAndOnPropertiesChange(this::updateVisibleActions, Collections.map(actions, Action::visibleProperty));
@@ -40,7 +41,7 @@ private void updateVisibleActions() {
for (Action action : this.actions) {
if (action.isVisible()) {
int n = actions.size();
- if (action.getText() == null && action.getGraphic() == null && action instanceof ActionGroup) {
+ if (action.getText() == null && action.getGraphicFactory() == null && action instanceof ActionGroup) {
ActionGroup actionGroup = (ActionGroup) action;
actions.addAll(actionGroup.getVisibleActions());
if (actions.size() > n) {
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java
index 17d90a782..c0843f2b0 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/ReadOnlyAction.java
@@ -1,12 +1,14 @@
package dev.webfx.stack.ui.action.impl;
+import dev.webfx.stack.ui.action.Action;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
-import dev.webfx.stack.ui.action.Action;
+
+import java.util.function.Supplier;
/**
* A read-only action where properties are observable values.
@@ -15,9 +17,9 @@
*/
public class ReadOnlyAction implements Action {
- public ReadOnlyAction(ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) {
+ public ReadOnlyAction(ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) {
this.textProperty = textProperty;
- this.graphicProperty = graphicProperty;
+ this.graphicFactoryProperty = graphicFactoryProperty;
this.disabledProperty = disabledProperty;
this.visibleProperty = visibleProperty;
this.actionHandler = actionHandler;
@@ -29,10 +31,10 @@ public ObservableStringValue textProperty() {
return textProperty;
}
- private final ObservableObjectValue graphicProperty;
+ private final ObservableValue> graphicFactoryProperty;
@Override
- public ObservableObjectValue graphicProperty() {
- return graphicProperty;
+ public ObservableValue> graphicFactoryProperty() {
+ return graphicFactoryProperty;
}
private final ObservableBooleanValue disabledProperty;
diff --git a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java
index 849ef13ca..8f42b2cee 100644
--- a/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java
+++ b/webfx-stack-ui-action/src/main/java/dev/webfx/stack/ui/action/impl/WritableAction.java
@@ -1,14 +1,16 @@
package dev.webfx.stack.ui.action.impl;
+import dev.webfx.platform.util.Arrays;
+import dev.webfx.stack.ui.action.Action;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
-import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
-import dev.webfx.stack.ui.action.Action;
-import dev.webfx.platform.util.Arrays;
+
+import java.util.function.Supplier;
/**
* A writable action where properties (text, graphic, disabled, visible) can be set later (ie after constructor call)
@@ -19,15 +21,15 @@
public class WritableAction extends ReadOnlyAction {
public WritableAction(Action action, String... writablePropertyNames) {
- this(createStringProperty(action.textProperty(), "text", writablePropertyNames), createObjectProperty(action.graphicProperty(), "graphic", writablePropertyNames), createBooleanProperty(action.disabledProperty(), "disabled", writablePropertyNames), createBooleanProperty(action.visibleProperty(), "visible", writablePropertyNames), action);
+ this(createStringProperty(action.textProperty(), "text", writablePropertyNames), createObjectProperty(action.graphicFactoryProperty(), "graphicFactory", writablePropertyNames), createBooleanProperty(action.disabledProperty(), "disabled", writablePropertyNames), createBooleanProperty(action.visibleProperty(), "visible", writablePropertyNames), action);
}
public WritableAction(EventHandler actionHandler) {
this(new SimpleStringProperty(), new SimpleObjectProperty<>(), new SimpleBooleanProperty(true /* disabled until it is bound */), new SimpleBooleanProperty(false /* invisible until it is bound */), actionHandler);
}
- public WritableAction(ObservableStringValue textProperty, ObservableObjectValue graphicProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) {
- super(textProperty, graphicProperty, disabledProperty, visibleProperty, actionHandler);
+ public WritableAction(ObservableStringValue textProperty, ObservableValue> graphicFactoryProperty, ObservableBooleanValue disabledProperty, ObservableBooleanValue visibleProperty, EventHandler actionHandler) {
+ super(textProperty, graphicFactoryProperty, disabledProperty, visibleProperty, actionHandler);
}
public StringProperty writableTextProperty() {
@@ -38,12 +40,12 @@ public void setText(String text) {
writableTextProperty().set(text);
}
- public ObjectProperty writableGraphicProperty() {
- return (ObjectProperty) graphicProperty();
+ public ObjectProperty> writableGraphicFactoryProperty() {
+ return (ObjectProperty>) graphicFactoryProperty();
}
- public void setGraphic(Node graphic) {
- writableGraphicProperty().set(graphic);
+ public void setGraphicFactory(Supplier graphicFactory) {
+ writableGraphicFactoryProperty().set(graphicFactory);
}
public BooleanProperty writableDisabledProperty() {
@@ -76,10 +78,10 @@ public void set(String newValue) {
return writableProperty;
}
- private static ObjectProperty createObjectProperty(ObservableObjectValue readOnlyProperty, String propertyName, String... writablePropertyNames) {
+ private static ObjectProperty createObjectProperty(ObservableValue readOnlyProperty, String propertyName, String... writablePropertyNames) {
if (readOnlyProperty instanceof ObjectProperty)
return (ObjectProperty) readOnlyProperty;
- SimpleObjectProperty writableProperty = new SimpleObjectProperty() {
+ SimpleObjectProperty writableProperty = new SimpleObjectProperty<>() {
@Override
public void set(T newValue) {
unbindPropertyIfWritable(this, propertyName, writablePropertyNames);
diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java
index dcdeb5c0e..061e99672 100644
--- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java
+++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/ControlFactoryMixin.java
@@ -110,11 +110,11 @@ default Button styleButton(Button button) {
}
default CheckBox newCheckBox(Object i18nKey) {
- return I18nControls.bindI18nProperties(new CheckBox(), i18nKey);
+ return I18nControls.newCheckBox(i18nKey);
}
default RadioButton newRadioButton(Object i18nKey) {
- return I18nControls.bindI18nProperties(new RadioButton(), i18nKey);
+ return I18nControls.newRadioButton(i18nKey);
}
default RadioButton newRadioButton(Object i18nKey, ToggleGroup toggleGroup) {
@@ -124,7 +124,7 @@ default RadioButton newRadioButton(Object i18nKey, ToggleGroup toggleGroup) {
}
default Label newLabel(Object i18nKey) {
- return I18nControls.bindI18nProperties(new Label(), i18nKey);
+ return I18nControls.newLabel(i18nKey);
}
default TextField newTextField() {
@@ -144,7 +144,7 @@ default Hyperlink newHyperlink() {
}
default Hyperlink newHyperlink(Object i18nKey) {
- return I18nControls.bindI18nProperties(newHyperlink(), i18nKey);
+ return I18nControls.newHyperlink(i18nKey);
}
default Hyperlink newHyperlink(Object i18nKey, EventHandler onAction) {
@@ -158,7 +158,7 @@ default TextArea newTextArea(Object i18nKey) {
}
default Text newText(Object i18nKey) {
- return I18n.bindI18nProperties(new Text(), i18nKey);
+ return I18n.newText(i18nKey);
}
}
diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java
index bcd8188e9..b52cd46d5 100644
--- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java
+++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/button/ButtonFactory.java
@@ -62,7 +62,7 @@ public static Button decorateButtonWithDropDownArrow(Button button) {
downArrow.setFill(null);
downArrow.setContent("M1 1.22998L6.325 6.55499L11.65 1.22998");
GraphicDecoration dropDownArrowDecoration = new GraphicDecoration(downArrow, Pos.CENTER_RIGHT, 0, 0, -1, 0);
- FXProperties.runNowAndOnPropertiesChange(() -> Platform.runLater(() ->
+ FXProperties.runNowAndOnPropertyChange(() -> Platform.runLater(() ->
Controls.onSkinReady(button, () -> dropDownArrowDecoration.applyDecoration(button))
), button.graphicProperty());
// Code to clip the content before the down arrow
diff --git a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java
index 73739f4ee..c0b7e8c82 100644
--- a/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java
+++ b/webfx-stack-ui-controls/src/main/java/dev/webfx/stack/ui/controls/dialog/DialogContent.java
@@ -72,6 +72,12 @@ public DialogContent setYesNo() {
return this;
}
+ public DialogContent setOk() {
+ primaryButtonText = "Ok";
+ secondaryButton.setManaged(false);
+ return this;
+ }
+
public Button getPrimaryButton() {
return primaryButton;
}
diff --git a/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java b/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java
index 8d2154fd0..3bd74ee98 100644
--- a/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java
+++ b/webfx-stack-ui-json/src/main/java/dev/webfx/stack/ui/json/JsonSVGPath.java
@@ -7,6 +7,8 @@
import javafx.scene.paint.Paint;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.SVGPath;
+import javafx.scene.shape.StrokeLineCap;
+import javafx.scene.shape.StrokeLineJoin;
/**
* @author Bruno Salmon
@@ -21,6 +23,9 @@ public static SVGPath createSVGPath(ReadOnlyAstObject json) {
svgPath.setStroke(toPaint(json.getString("stroke"), null));
svgPath.setStrokeWidth(json.getDouble("strokeWidth", 1d));
svgPath.setFillRule(toFillRule(json.getString("fillRule")));
+ svgPath.setStrokeLineCap(toStrokeLineCap(json.getString("strokeLineCap")));
+ svgPath.setStrokeLineJoin(toStrokeLineJoin(json.getString("strokeLineJoin")));
+ svgPath.setStrokeMiterLimit(json.getDouble("strokeMiterLimit", 10d));
return svgPath;
}
@@ -40,11 +45,36 @@ private static Paint toPaint(String paintText, Paint defaultPaint) {
}
private static FillRule toFillRule(String ruleText) {
- if (ruleText != null)
+ if (ruleText != null) {
switch (ruleText.trim().toLowerCase()) {
- case "evenodd" : return FillRule.EVEN_ODD;
- case "nonzero" : return FillRule.NON_ZERO;
+ case "evenodd":
+ return FillRule.EVEN_ODD;
+ case "nonzero":
+ return FillRule.NON_ZERO;
}
+ }
return FillRule.NON_ZERO;
}
+
+ private static StrokeLineCap toStrokeLineCap(String strokeLineCapText) {
+ if (strokeLineCapText != null) {
+ switch (strokeLineCapText.trim().toLowerCase()) {
+ case "square" : return StrokeLineCap.SQUARE;
+ case "butt" : return StrokeLineCap.BUTT;
+ case "round" : return StrokeLineCap.ROUND;
+ }
+ }
+ return StrokeLineCap.SQUARE;
+ }
+
+ private static StrokeLineJoin toStrokeLineJoin(String StrokeLineJoinText) {
+ if (StrokeLineJoinText != null) {
+ switch (StrokeLineJoinText.trim().toLowerCase()) {
+ case "miter": return StrokeLineJoin.MITER;
+ case "bevel": return StrokeLineJoin.BEVEL;
+ case "round": return StrokeLineJoin.ROUND;
+ }
+ }
+ return StrokeLineJoin.MITER;
+ }
}
diff --git a/webfx-stack-ui-operation-action/pom.xml b/webfx-stack-ui-operation-action/pom.xml
index b65dbafb7..d7388c4a9 100644
--- a/webfx-stack-ui-operation-action/pom.xml
+++ b/webfx-stack-ui-operation-action/pom.xml
@@ -45,6 +45,14 @@
0.1.0-SNAPSHOT
+
+ dev.webfx
+ webfx-platform-javabase-emul-j2cl
+ 0.1.0-SNAPSHOT
+ runtime
+ true
+
+
dev.webfx
webfx-platform-javatime-emul-j2cl
@@ -53,6 +61,12 @@
true
+
+ dev.webfx
+ webfx-platform-scheduler
+ 0.1.0-SNAPSHOT
+
+
dev.webfx
webfx-platform-uischeduler
@@ -77,6 +91,12 @@
0.1.0-SNAPSHOT
+
+ dev.webfx
+ webfx-stack-ui-action-tuner
+ 0.1.0-SNAPSHOT
+
+
dev.webfx
webfx-stack-ui-exceptions
diff --git a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java
index cb8aaf170..de427612b 100644
--- a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java
+++ b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationAction.java
@@ -58,13 +58,9 @@ private OperationAction(OperationAction[] me, Function
me[0] = this;
this.operationRequestFactory = operationRequestFactory;
OperationActionRegistry registry = getOperationActionRegistry();
- registry.bindOperationActionGraphicalProperties(this);
- // Also, if some graphical dependencies are passed, we update the graphical properties when they change
- if (graphicalDependencies.length > 0)
- FXProperties.runNowAndOnPropertiesChange(() -> {
- registry.updateOperationActionGraphicalProperties(this);
- registry.bindOperationActionGraphicalProperties(this);
- }, graphicalDependencies);
+ FXProperties.runNowAndOnPropertiesChange(() ->
+ registry.bindOperationActionGraphicalProperties(this)
+ , graphicalDependencies); // Also updating the graphical properties when graphical dependencies change
}
public OperationActionRegistry getOperationActionRegistry() {
@@ -86,9 +82,8 @@ private void startShowingActionAsExecuting(Object operationRequest) {
// If in addition an icon has been provided to graphically indicate the execution is in progress,
if (actionExecutingIconFactory != null) { // we apply it to the graphic property
Node executingIcon = actionExecutingIconFactory.apply(operationRequest);
- if (executingIcon != null) {
- FXProperties.setEvenIfBound(writableGraphicProperty(), executingIcon);
- }
+ if (executingIcon != null) // For some operations such as routing operation, there is no executing icon
+ FXProperties.setEvenIfBound(writableGraphicFactoryProperty(), () -> executingIcon);
}
}
@@ -100,9 +95,8 @@ private void stopShowingActionAsExecuting(Object operationRequest, Throwable exc
// If in addition an icon has been provided to graphically indicate the execution has ended,
if (actionExecutedIconFactory != null) { // we apply it to the graphic property for 2s
Node executedIcon = actionExecutedIconFactory.apply(operationRequest, exception);
- if (executedIcon != null) {
- FXProperties.setEvenIfBound(writableGraphicProperty(), executedIcon);
- }
+ if (executedIcon != null) // For some operations such as routing operation, there is no executed icon
+ FXProperties.setEvenIfBound(writableGraphicFactoryProperty(), () -> executedIcon);
UiScheduler.scheduleDelay(2000, () -> {
// After 2 seconds, we reestablish the original action icon, unless it's already executing again
if (!executing) { // if executing again, we keep the possible executing icon instead
diff --git a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java
index 90e74f406..0691fe3da 100644
--- a/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java
+++ b/webfx-stack-ui-operation-action/src/main/java/dev/webfx/stack/ui/operation/action/OperationActionRegistry.java
@@ -1,11 +1,17 @@
package dev.webfx.stack.ui.operation.action;
+import dev.webfx.kit.util.properties.FXProperties;
import dev.webfx.platform.async.AsyncFunction;
import dev.webfx.platform.async.Future;
+import dev.webfx.platform.console.Console;
+import dev.webfx.platform.scheduler.Scheduled;
import dev.webfx.platform.uischeduler.UiScheduler;
+import dev.webfx.platform.util.collection.Collections;
import dev.webfx.stack.authz.client.factory.AuthorizationUtil;
import dev.webfx.stack.ui.action.Action;
import dev.webfx.stack.ui.action.ActionBinder;
+import dev.webfx.stack.ui.action.impl.WritableAction;
+import dev.webfx.stack.ui.action.tuner.ActionTuner;
import dev.webfx.stack.ui.operation.HasOperationCode;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -13,10 +19,8 @@
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
+import java.lang.ref.WeakReference;
+import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -41,17 +45,32 @@
*/
public final class OperationActionRegistry {
- private static OperationActionRegistry INSTANCE;
+ private static final boolean LOG_DEBUG = false;
- private final Map
+
+ dev.webfx
+ webfx-extras-util-control
+ 0.1.0-SNAPSHOT
+
+
dev.webfx
webfx-kit-util
diff --git a/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java b/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java
index 0deb82f94..d2beb5692 100644
--- a/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java
+++ b/webfx-stack-ui-operation/src/main/java/dev/webfx/stack/ui/operation/OperationUtil.java
@@ -1,12 +1,12 @@
package dev.webfx.stack.ui.operation;
+import dev.webfx.extras.util.control.ControlUtil;
import dev.webfx.kit.util.properties.FXProperties;
import dev.webfx.platform.async.AsyncFunction;
import dev.webfx.platform.async.Future;
import dev.webfx.platform.uischeduler.UiScheduler;
import javafx.scene.Node;
import javafx.scene.control.Labeled;
-import javafx.scene.control.ProgressIndicator;
/**
* @author Bruno Salmon
@@ -48,9 +48,7 @@ private static void setWaitMode(boolean on, Labeled... buttons) {
Node graphic = null;
if (button == buttons[0]) {
if (on) {
- ProgressIndicator progressIndicator = new ProgressIndicator();
- progressIndicator.setPrefSize(20, 20); // Note setMaxSize() doesn't work with WebFX but setPrefSize() does
- graphic = progressIndicator;
+ graphic = ControlUtil.createProgressIndicator(20);
// Memorising the previous graphic before changing it
button.getProperties().put("webfx-operation-util-graphic", button.getGraphic());
} else {
diff --git a/webfx-stack-ui-operation/src/main/java/module-info.java b/webfx-stack-ui-operation/src/main/java/module-info.java
index ce97e7869..367c34a3b 100644
--- a/webfx-stack-ui-operation/src/main/java/module-info.java
+++ b/webfx-stack-ui-operation/src/main/java/module-info.java
@@ -5,6 +5,7 @@
// Direct dependencies modules
requires javafx.controls;
requires javafx.graphics;
+ requires webfx.extras.util.control;
requires webfx.kit.util;
requires webfx.platform.async;
requires webfx.platform.uischeduler;
diff --git a/webfx-stack-ui-validation/pom.xml b/webfx-stack-ui-validation/pom.xml
index 9d625dd07..1014ed9ea 100644
--- a/webfx-stack-ui-validation/pom.xml
+++ b/webfx-stack-ui-validation/pom.xml
@@ -33,6 +33,44 @@
provided
+
+ dev.webfx
+ webfx-extras-imagestore
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-extras-util-background
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-extras-util-border
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-extras-util-scene
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-kit-util
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-platform-javabase-emul-j2cl
+ 0.1.0-SNAPSHOT
+ runtime
+ true
+
+
dev.webfx
webfx-platform-javatime-emul-j2cl
@@ -41,6 +79,18 @@
true
+
+ dev.webfx
+ webfx-platform-uischeduler
+ 0.1.0-SNAPSHOT
+
+
+
+ dev.webfx
+ webfx-platform-util
+ 0.1.0-SNAPSHOT
+
+
\ No newline at end of file
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java
new file mode 100644
index 000000000..5ef3b7991
--- /dev/null
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationIcons.java
@@ -0,0 +1,11 @@
+package dev.webfx.stack.ui.validation;
+
+/**
+ * @author Bruno Salmon
+ */
+final class ValidationIcons {
+
+ public final static String validationErrorIcon16Url = "dev/webfx/stack/ui/validation/controlsfx/images/decoration-error.png";
+ public final static String validationRequiredIcon16Url = "dev/webfx/stack/ui/validation/controlsfx/images/required-indicator.png";
+
+}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java
new file mode 100644
index 000000000..bb2f74b8b
--- /dev/null
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/ValidationSupport.java
@@ -0,0 +1,558 @@
+package dev.webfx.stack.ui.validation;
+
+import dev.webfx.extras.imagestore.ImageStore;
+import dev.webfx.extras.util.background.BackgroundFactory;
+import dev.webfx.extras.util.border.BorderFactory;
+import dev.webfx.extras.util.scene.SceneUtil;
+import dev.webfx.kit.util.properties.FXProperties;
+import dev.webfx.platform.uischeduler.UiScheduler;
+import dev.webfx.platform.util.collection.Collections;
+import dev.webfx.stack.ui.validation.controlsfx.control.decoration.Decoration;
+import dev.webfx.stack.ui.validation.controlsfx.control.decoration.GraphicDecoration;
+import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.GraphicValidationDecoration;
+import dev.webfx.stack.ui.validation.mvvmfx.ObservableRuleBasedValidator;
+import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage;
+import dev.webfx.stack.ui.validation.mvvmfx.Validator;
+import dev.webfx.stack.ui.validation.mvvmfx.visualization.ControlsFxVisualizer;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.*;
+import javafx.beans.value.ObservableBooleanValue;
+import javafx.beans.value.ObservableStringValue;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.text.Font;
+import javafx.scene.transform.Rotate;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * @author Bruno Salmon
+ */
+public final class ValidationSupport {
+
+ private static final ObservableStringValue DEFAULT_REQUIRED_MESSAGE = new SimpleStringProperty("This field is required");
+
+ private final List validators = new ArrayList<>();
+ private final List validatorErrorDecorationNodes = new ArrayList<>();
+ private final BooleanProperty validatingProperty = new SimpleBooleanProperty();
+ private Node popOverContentNode;
+ private Node popOverOwnerNode;
+
+ public boolean isValid() {
+ validatingProperty.setValue(false);
+ validatingProperty.setValue(true);
+ Validator firstInvalidValidator = firstInvalidValidator();
+ if (firstInvalidValidator != null)
+ Platform.runLater(() -> {
+ popUpOverAutoScroll = true;
+ showValidatorErrorPopOver(firstInvalidValidator);
+ });
+ return firstInvalidValidator == null;
+ }
+
+ public void reset() {
+ // This will hide the possible validation error popup and other warning icons
+ validatingProperty.setValue(false);
+ }
+
+ public void clear() {
+ reset(); // hiding possible validation messages
+ validatorErrorDecorationNodes.forEach(this::uninstallNodeDecorator);
+ validatorErrorDecorationNodes.clear();
+ validators.forEach(validator -> {
+ if (validator instanceof ObservableRuleBasedValidator) {
+ ((ObservableRuleBasedValidator) validator).clear();
+ }
+ });
+ validators.clear();
+ }
+
+ public boolean isEmpty() {
+ return validators.isEmpty();
+ }
+
+ private Validator firstInvalidValidator() {
+ return Collections.findFirst(validators, validator -> !validator.getValidationStatus().isValid());
+ }
+
+ public void addRequiredInputs(TextInputControl... textInputControls) {
+ for (TextInputControl textInputControl : textInputControls)
+ addRequiredInput(textInputControl);
+ }
+
+ public void addRequiredInput(TextInputControl textInputControl) {
+ addRequiredInput(textInputControl, DEFAULT_REQUIRED_MESSAGE);
+ }
+
+ public void addRequiredInput(TextInputControl textInputControl, ObservableStringValue errorMessage) {
+ addRequiredInput(textInputControl.textProperty(), textInputControl, errorMessage);
+ }
+
+ public void addRequiredInput(ObservableValue> valueProperty, Node inputNode) {
+ addRequiredInput(valueProperty, inputNode, DEFAULT_REQUIRED_MESSAGE);
+ }
+
+ public void addRequiredInput(ObservableValue> valueProperty, Node inputNode, ObservableStringValue errorMessage) {
+ addValidationRule(Bindings.createBooleanBinding(() -> testNotEmpty(valueProperty.getValue()), valueProperty), inputNode, errorMessage, true);
+ }
+
+ private static boolean testNotEmpty(Object value) {
+ return value != null && (!(value instanceof String) || !((String) value).trim().isEmpty());
+ }
+
+ public ObservableBooleanValue addValidationRule(ObservableValue validProperty, Node node, ObservableStringValue errorMessage) {
+ return addValidationRule(validProperty, node, errorMessage, false);
+ }
+
+ public ObservableBooleanValue addValidationRule(ObservableValue validProperty, Node node, ObservableStringValue errorMessageProperty, boolean required) {
+ ObservableRuleBasedValidator validator = new ObservableRuleBasedValidator();
+ ObservableBooleanValue finalValidProperty = // when true, no decorations are displayed on the node
+ Bindings.createBooleanBinding(() ->
+ !validatingProperty.get() // no decoration displayed if not validation
+ || validProperty.getValue() // no decoration displayed if the node is valid
+ || !isShowing(node) // no decoration displayed if the node is not showing
+ , validProperty, validatingProperty);
+ ValidationMessage errorValidationMessage = ValidationMessage.error(errorMessageProperty);
+ validator.addRule(finalValidProperty, errorValidationMessage);
+ validators.add(validator);
+ validatorErrorDecorationNodes.add(node);
+ installNodeDecorator(node, validator, required);
+ // The following code is to remove the error message after being displayed and the user is re-typing
+ if (node instanceof TextInputControl) {
+ FXProperties.runOnPropertiesChange(
+ () -> {
+ validator.validateBooleanRule(true, errorValidationMessage);
+ uninstallNodeDecorator(node);
+ }
+ , ((TextInputControl) node).textProperty()); // ex of dependencies: textField.textProperty()
+ }
+ return finalValidProperty;
+ }
+
+ private void installNodeDecorator(Node node, ObservableRuleBasedValidator validator, boolean required) {
+ if (node instanceof Control) {
+ Control control = (Control) node;
+ ControlsFxVisualizer validationVisualizer = new ControlsFxVisualizer();
+ validationVisualizer.setDecoration(new GraphicValidationDecoration() {
+ @Override
+ protected Node createErrorNode() {
+ return ImageStore.createImageView(ValidationIcons.validationErrorIcon16Url);
+ }
+
+ @Override
+ protected Collection createValidationDecorations(dev.webfx.stack.ui.validation.controlsfx.validation.ValidationMessage message) {
+ boolean isTextInput = node instanceof TextInputControl;
+ boolean isButton = node instanceof Button;
+ // isInside flag will determine if we position the decoration inside the node or not (ie outside)
+ boolean isInside;
+ if (isTextInput) // inside for text inputs
+ isInside = true;
+ else { // for others, will be generally outside unless it is stretched to full width by its container
+ Parent parent = node.getParent();
+ while (parent instanceof Pane && !(parent instanceof VBox) && !(parent instanceof HBox))
+ parent = parent.getParent();
+ isInside = parent instanceof VBox && ((VBox) parent).isFillWidth();
+ }
+ double xRelativeOffset = isInside ? -1 : 1; // positioning the decoration inside the control for button and text input
+ double xOffset = isInside && isButton ? -20 : 0; // moving the decoration before the drop down arrow
+ return java.util.Collections.singletonList(
+ new GraphicDecoration(createDecorationNode(message),
+ Pos.CENTER_RIGHT,
+ xOffset,
+ 0,
+ xRelativeOffset,
+ 0)
+ );
+ }
+
+ @Override
+ protected Collection createRequiredDecorations(Control target) {
+ return java.util.Collections.singletonList(
+ new GraphicDecoration(ImageStore.createImageView(ValidationIcons.validationRequiredIcon16Url),
+ Pos.CENTER_LEFT,
+ -10,
+ 0));
+ }
+ });
+ validationVisualizer.initVisualization(validator.getValidationStatus(), control, required);
+ node.getProperties().put("validationVisualizer", validationVisualizer);
+ }
+ }
+
+ private void uninstallNodeDecorator(Node node) {
+ if (node instanceof Control) {
+ Control control = (Control) node;
+ ControlsFxVisualizer validationVisualizer = (ControlsFxVisualizer) node.getProperties().get("validationVisualizer");
+ if (validationVisualizer != null)
+ validationVisualizer.removeDecorations(control);
+ }
+ }
+
+ private void showValidatorErrorPopOver(Validator validator) {
+ int index = validators.indexOf(validator);
+ if (index >= 0) {
+ Node decorationNode = validatorErrorDecorationNodes.get(index);
+ if (decorationNode != null)
+ showValidatorErrorPopOver(validator, decorationNode);
+ }
+ }
+
+ private void showValidatorErrorPopOver(Validator validator, Node errorDecorationNode) {
+ ValidationMessage errorMessage = Collections.first(validator.getValidationStatus().getErrorMessages());
+ if (errorMessage != null) {
+ Label label = new Label();
+ label.textProperty().bind(errorMessage.messageProperty());
+ label.setPadding(new Insets(8));
+ label.setFont(Font.font("Verdana", 11.5));
+ label.setTextFill(Color.WHITE);
+ label.setBackground(BackgroundFactory.newBackground(Color.RED, 5, 2));
+ label.setBorder(BorderFactory.newBorder(Color.WHITE, 5, 2));
+ Rectangle diamond = new Rectangle(10, 10, Color.RED);
+ diamond.getTransforms().add(new Rotate(45, 5, 5));
+ diamond.layoutYProperty().bind(FXProperties.compute(label.heightProperty(), n -> n.doubleValue() - 7));
+ diamond.setLayoutX(20d);
+ popOverContentNode = new Group(label, diamond);
+ //popOverContentNode.setOpacity(0.75);
+ //popOverContentNode.setEffect(new DropShadow());
+ showPopOver(errorDecorationNode);
+ // Removing the error pop over when the status is valid again
+ FXProperties.runOrUnregisterOnPropertyChange((thisListener, oldValue, valid) -> {
+ if (valid) {
+ thisListener.unregister();
+ popOverOwnerNode = null;
+ hidePopOver();
+ }
+ }, validator.getValidationStatus().validProperty());
+ }
+ }
+
+ private void showPopOver(Node node) {
+ popOverOwnerNode = node;
+ showPopOverNow();
+ if (!node.getProperties().containsKey("popOverListen")) {
+ node.getProperties().put("popOverListen", true);
+ node.sceneProperty().addListener(observable -> {
+ if (popOverOwnerNode == node) {
+ showPopOverNow();
+ }
+ });
+ node.parentProperty().addListener(observable -> {
+ if (popOverOwnerNode == node) {
+ showPopOverNow();
+ }
+ });
+ }
+ }
+
+ public void addEmailValidation(TextField emailInput, Node where, ObservableStringValue errorMessage) {
+ // Define the email pattern
+ String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$";
+ Pattern pattern = Pattern.compile(emailPattern);
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> pattern.matcher(emailInput.getText()).matches(),
+ emailInput.textProperty()
+ ),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addEmailNotEqualValidation(TextField emailInput, String forbiddenValue, Node where, ObservableStringValue errorMessage) {
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> !emailInput.getText().equalsIgnoreCase(forbiddenValue),
+ emailInput.textProperty()
+ ),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addUrlValidation(TextField urlInput, Node where, ObservableStringValue errorMessage) {
+ // Define the URL pattern (basic)
+ String urlPattern = "^(https?://)(www\\.)?[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}(/.*)?$";
+ Pattern pattern = Pattern.compile(urlPattern);
+
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> {
+ String input = urlInput.getText();
+ return input!=null && pattern.matcher(input).matches(); // Accept empty or valid URL
+ },
+ urlInput.textProperty()
+ ),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addMinimumDurationValidation(TextField timeInput, Node where, ObservableStringValue errorMessage) {
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> {
+ try {
+ String input = timeInput.getText();
+ if (input == null || input.isEmpty()) return true; // Allow empty input
+ int value = Integer.parseInt(input); // Try to parse the input to an integer
+ return value >= 60; // Validate if the integer is at least 60
+ } catch (NumberFormatException e) {
+ return false; // Invalid if input is not a valid integer
+ }
+ },
+ timeInput.textProperty()
+ ),
+ where,
+ errorMessage
+ );
+ }
+
+
+ public void addUrlOrEmptyValidation(TextField urlInput, ObservableStringValue errorMessage) {
+ // Define the URL pattern (basic)
+ String urlPattern = "^(https?|srt|rtmp|rtsp)://[\\w.-]+(:\\d+)?(/[\\w./-]*)?(\\?[\\w=&%.-]*)?(#[\\w!:.=&,-]*)?$";
+ Pattern pattern = Pattern.compile(urlPattern);
+
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> {
+ String input = urlInput.getText();
+ return input.isEmpty() || pattern.matcher(input).matches(); // Accept empty or valid URL
+ },
+ urlInput.textProperty()
+ ),
+ urlInput,
+ errorMessage
+ );
+ }
+
+ public void addNonEmptyValidation(TextField textField, Node where, ObservableStringValue errorMessage) {
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> !textField.getText().trim().isEmpty(),
+ textField.textProperty()
+ ),
+ where,
+ errorMessage);
+ }
+
+ public void addDateValidation(TextField textField,String dateFormat, Node where, ObservableStringValue errorMessage) {
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(() -> {
+ try {
+ dateFormatter.parse(textField.getText().trim());
+ return true;
+ } catch (DateTimeParseException e) {
+ return false;
+ }}, textField.textProperty()),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addDateOrEmptyValidation(TextField textField, String dateFormat, Node where, ObservableStringValue errorMessage) {
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
+
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(() -> {
+ String input = textField.getText().trim();
+ // Allow empty input to be valid
+ if (input.isEmpty()) {
+ return true;
+ }
+ try {
+ dateFormatter.parse(input);
+ return true; // Input is valid if it can be parsed
+ } catch (DateTimeParseException e) {
+ return false; // Invalid date format
+ }
+ }, textField.textProperty()),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addIntegerValidation(TextField textField, Node where, ObservableStringValue errorMessage) {
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(() -> {
+ try {
+ // Try to parse the text as an integer
+ Integer.parseInt(textField.getText().trim());
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }, textField.textProperty()),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addLegalAgeValidation(TextField textField, String dateFormat, int legalAge, Node where, ObservableStringValue errorMessage) {
+ DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat);
+ // Create the validation rule
+ addValidationRule(
+ Bindings.createBooleanBinding(() -> {
+ try {
+ LocalDate birthDate = LocalDate.parse(textField.getText().trim(), dateFormatter);
+ LocalDate now = LocalDate.now();
+ return birthDate.plusYears(legalAge).isBefore(now) || birthDate.plusYears(legalAge).isEqual(now);
+ } catch (DateTimeParseException e) {
+ return false;
+ }
+ }, textField.textProperty()),
+ where,
+ errorMessage
+ );
+ }
+
+ public void addPasswordMatchValidation(TextField passwordField, TextField repeatPasswordField, ObservableStringValue errorMessageProperty) {
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> passwordField.getText().equals(repeatPasswordField.getText()),
+ passwordField.textProperty(),
+ repeatPasswordField.textProperty()
+ ),
+ repeatPasswordField,
+ errorMessageProperty,
+ true
+ );
+ }
+
+ public void addPasswordStrengthValidation(TextField passwordField, ObservableStringValue errorMessage) {
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> checkPasswordStrength(passwordField.getText()),
+ passwordField.textProperty()
+ ),
+ passwordField,
+ errorMessage,
+ true
+ );
+ }
+ /**
+ * Checks if a password meets strength requirements.
+ * @param password the password to validate.
+ * @return true if the password meets the requirements, false otherwise.
+ */
+ private boolean checkPasswordStrength(String password) {
+ if (password == null || password.isEmpty()) {
+ return false;
+ }
+ // Minimum length
+ if (password.length() < 8) {
+ return false;
+ }
+ // Contains at least one uppercase letter
+ if (!password.matches(".*[A-Z].*")) {
+ return false;
+ }
+ // Contains at least one lowercase letter
+ if (!password.matches(".*[a-z].*")) {
+ return false;
+ }
+ // Contains at least one digit
+ if (!password.matches(".*\\d.*")) {
+ return false;
+ }
+ // Contains at least one special character
+ if (!password.matches(".*[@#$%^&+=!().,*?-].*")) {
+ return false;
+ }
+ return true;
+ }
+
+/*
+ private PopOver popOver;
+ private void showPopOverNow() {
+ if (popOver != null && popOver.getOwnerNode() != popOverOwnerNode) {
+ popOver.hide();
+ popOver = null;
+ }
+ if (popOver == null && isShowing(popOverOwnerNode)) {
+ popOver = new PopOver();
+ popOver.setContentNode(popOverContentNode);
+ popOver.setArrowLocation(PopOver.ArrowLocation.BOTTOM_LEFT);
+ //Platform.runLater(() -> {
+ popOver.show(popOverOwnerNode, -(popOverOwnerNode instanceof ImageView ? ((ImageView) popOverOwnerNode).getImage().getHeight() : 0) + 4);
+ //});
+ }
+ }
+*/
+
+ private GraphicDecoration popOverDecoration;
+ private Node popOverDecorationTarget;
+ private boolean popUpOverAutoScroll;
+
+ private void showPopOverNow() {
+ Platform.runLater(() -> {
+ hidePopOver();
+ if (isShowing(popOverOwnerNode)) {
+ popOverDecorationTarget = popOverOwnerNode;
+ popOverDecoration = new GraphicDecoration(popOverContentNode, 0, -1, 0, -1);
+ popOverDecoration.applyDecoration(popOverDecorationTarget);
+ if (popUpOverAutoScroll) {
+ SceneUtil.scrollNodeToBeVerticallyVisibleOnScene(popOverDecorationTarget);
+ SceneUtil.autoFocusIfEnabled(popOverOwnerNode);
+ popUpOverAutoScroll = false;
+ }
+ }
+ });
+ }
+
+ private void hidePopOver() {
+ UiScheduler.runInUiThread(() -> {
+ if (popOverDecoration != null) {
+ popOverDecoration.removeDecoration(popOverDecorationTarget);
+ popOverDecoration = null;
+ }
+ });
+ }
+
+ private static boolean isShowing(Node node) {
+ if (node == null || !node.isVisible())
+ return false;
+ Parent parent = node.getParent();
+ if (parent != null)
+ return isShowing(parent);
+ Scene scene = node.getScene();
+ return scene != null && scene.getRoot() == node;
+ }
+
+ public void addPasswordValidation(TextField passwordInput, Label passwordLabel, ObservableStringValue errorMessage) {
+ addValidationRule(
+ Bindings.createBooleanBinding(
+ () -> passwordInput.getText().length() >= 8,
+ passwordInput.textProperty()
+ ),
+ passwordLabel,
+ errorMessage
+ );
+ }
+}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java
similarity index 99%
rename from webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java
rename to webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java
index e6c9d2396..ac68b6876 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationSupport.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ControlsFxValidationSupport.java
@@ -76,7 +76,7 @@
*
*
*/
-public class ValidationSupport {
+public class ControlsFxValidationSupport {
private static final String CTRL_REQUIRED_FLAG = "$org.controlsfx.validation.required$"; //$NON-NLS-1$
@@ -111,7 +111,7 @@ public static boolean isRequired( Control c ) {
* Creates validation support instance.
* If initial decoration is desired invoke {@link #initInitialDecoration()}.
*/
- public ValidationSupport() {
+ public ControlsFxValidationSupport() {
validationResultProperty().addListener( (o, oldValue, validationResult) -> {
invalidProperty.set(!validationResult.getErrors().isEmpty());
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java
index 45b699d6f..3ad4a0edb 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/ValidationMessage.java
@@ -35,28 +35,24 @@
*/
public interface ValidationMessage extends Comparable{
- public static final Comparator COMPARATOR = new Comparator() {
-
- @Override
- public int compare(ValidationMessage vm1, ValidationMessage vm2) {
- if ( vm1 == vm2 ) return 0;
- if ( vm1 == null) return 1;
- if ( vm2 == null) return -1;
- return vm1.compareTo(vm2);
- }
- };
+ Comparator COMPARATOR = (vm1, vm2) -> {
+ if ( vm1 == vm2 ) return 0;
+ if ( vm1 == null) return 1;
+ if ( vm2 == null) return -1;
+ return vm1.compareTo(vm2);
+ };
/**
* Message text
* @return message text
*/
- public String getText();
+ String getText();
/**
* Message {@link Severity}
* @return message severity
*/
- public Severity getSeverity();
+ Severity getSeverity();
/**
@@ -71,7 +67,7 @@ public int compare(ValidationMessage vm1, ValidationMessage vm2) {
* @param text message text
* @return error message
*/
- public static ValidationMessage error(Control target, String text) {
+ static ValidationMessage error(Control target, String text) {
return new SimpleValidationMessage(target, text, Severity.ERROR);
}
@@ -81,11 +77,11 @@ public static ValidationMessage error(Control target, String text) {
* @param text message text
* @return warning message
*/
- public static ValidationMessage warning(Control target, String text) {
+ static ValidationMessage warning(Control target, String text) {
return new SimpleValidationMessage(target, text, Severity.WARNING);
}
- @Override default public int compareTo(ValidationMessage msg) {
+ @Override default int compareTo(ValidationMessage msg) {
return msg == null || getTarget() != msg.getTarget() ? -1: getSeverity().compareTo(msg.getSeverity());
}
}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java
index a2011dd5d..8a9685232 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/controlsfx/validation/decoration/AbstractValidationDecoration.java
@@ -1,18 +1,18 @@
/**
* Copyright (c) 2014, 2015, ControlsFX
* All rights reserved.
- *
+ *
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
+ * * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
+ * * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
- * * Neither the name of ControlsFX, any associated website, nor the
+ * * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
- *
+ *
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -30,7 +30,7 @@
import dev.webfx.stack.ui.validation.controlsfx.control.decoration.Decorator;
import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationMessage;
import javafx.scene.control.Control;
-import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationSupport;
+import dev.webfx.stack.ui.validation.controlsfx.validation.ControlsFxValidationSupport;
import java.util.Collection;
import java.util.List;
@@ -42,67 +42,69 @@
* how 'validation' and 'required' decorations should be created
*
* See {@link GraphicValidationDecoration} for examples of such implementations.
- *
+ *
*/
public abstract class AbstractValidationDecoration implements ValidationDecoration {
-
- private static final String VALIDATION_DECORATION = "$org.controlsfx.decoration.vaidation$"; //$NON-NLS-1$
-
- private static boolean isValidationDecoration( Decoration decoration) {
+
+ private static final String VALIDATION_DECORATION = "$org.controlsfx.decoration.validation$"; //$NON-NLS-1$
+
+ private static boolean isValidationDecoration(Decoration decoration) {
return decoration != null && decoration.getProperties().get(VALIDATION_DECORATION) == Boolean.TRUE;
}
- private static void setValidationDecoration( Decoration decoration ) {
- if ( decoration != null ) {
+ private static void setValidationDecoration(Decoration decoration) {
+ if (decoration != null) {
decoration.getProperties().put(VALIDATION_DECORATION, Boolean.TRUE);
}
}
- protected abstract Collection createValidationDecorations(ValidationMessage message);
- protected abstract Collection createRequiredDecorations(Control target);
-
- /**
- * Removes all validation related decorations from the target
- * @param target control
- */
- @Override
- public void removeDecorations(Control target) {
- List decorations = Decorator.getDecorations(target);
- if ( decorations != null ) {
- // conversion to array is a trick to prevent concurrent modification exception
- for ( Decoration d: Decorator.getDecorations(target).toArray(new Decoration[0]) ) {
- if (isValidationDecoration(d)) Decorator.removeDecoration(target, d);
+ protected abstract Collection createValidationDecorations(ValidationMessage message);
+
+ protected abstract Collection createRequiredDecorations(Control target);
+
+ /**
+ * Removes all validation related decorations from the target
+ * @param target control
+ */
+ @Override
+ public void removeDecorations(Control target) {
+ List decorations = Decorator.getDecorations(target);
+ if (decorations != null) {
+ // conversion to array is a trick to prevent concurrent modification exception
+ for (Decoration d : Decorator.getDecorations(target).toArray(new Decoration[0])) {
+ if (isValidationDecoration(d))
+ Decorator.removeDecoration(target, d);
}
}
- }
-
- /*
- * (non-Javadoc)
- * @see org.controlsfx.validation.decorator.ValidationDecorator#applyValidationDecoration(org.controlsfx.validation.ValidationMessage)
- */
- @Override
- public void applyValidationDecoration(ValidationMessage message) {
- //createValidationDecorations(message).stream().forEach( d -> decorate( message.getTarget(), d )); // stream don't work on Android
- for (Decoration decoration : createValidationDecorations(message))
- decorate(message.getTarget(), decoration);
- }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.controlsfx.validation.decorator.ValidationDecorator#applyValidationDecoration(org.controlsfx.validation.ValidationMessage)
+ */
+ @Override
+ public void applyValidationDecoration(ValidationMessage message) {
+ //createValidationDecorations(message).stream().forEach( d -> decorate( message.getTarget(), d )); // stream don't work on Android
+ for (Decoration decoration : createValidationDecorations(message))
+ decorate(message.getTarget(), decoration);
+ }
- /*
- * (non-Javadoc)
- * @see org.controlsfx.validation.decorator.ValidationDecorator#applyRequiredDecoration(javafx.scene.control.Control)
- */
- @Override
- public void applyRequiredDecoration(Control target) {
- if ( ValidationSupport.isRequired(target)) {
- // createRequiredDecorations(target).stream().forEach( d -> decorate( target, d )); // stream don't work on Android
- for (Decoration decoration : createRequiredDecorations(target))
- decorate(target, decoration);
- }
- }
-
- private void decorate( Control target, Decoration d ) {
- setValidationDecoration(d); // mark as validation specific decoration
+ /*
+ * (non-Javadoc)
+ * @see org.controlsfx.validation.decorator.ValidationDecorator#applyRequiredDecoration(javafx.scene.control.Control)
+ */
+ @Override
+ public void applyRequiredDecoration(Control target) {
+ if (ControlsFxValidationSupport.isRequired(target)) {
+ // createRequiredDecorations(target).stream().forEach( d -> decorate( target, d )); // stream don't work on Android
+ for (Decoration decoration : createRequiredDecorations(target))
+ decorate(target, decoration);
+ }
+ }
+
+ private void decorate(Control target, Decoration d) {
+ setValidationDecoration(d); // mark as validation specific decoration
Decorator.addDecoration(target, d);
- }
+ }
}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java
index 8b5cb2aae..e72274e28 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ObservableRuleBasedValidator.java
@@ -15,6 +15,8 @@
******************************************************************************/
package dev.webfx.stack.ui.validation.mvvmfx;
+import dev.webfx.kit.util.properties.FXProperties;
+import dev.webfx.kit.util.properties.Unregisterable;
import javafx.beans.value.ObservableValue;
import java.util.ArrayList;
@@ -32,7 +34,7 @@
*
*
* Boolean Rules of type ObservableValue<Boolean>
- *
+ *
* The first variant uses an {@link ObservableValue} together with a static message. If this observable
* has a value of false the validation status will be "invalid" and the given message will be present in the {@link ValidationStatus}
* of this validator.
@@ -40,35 +42,27 @@
*
*
* Complex Rules of type ObservableValue<ValidationMessage>
- *
+ *
* The second variant allows more complex rules. It uses a {@link ObservableValue} as rule.
* If this observable has a value other then null it is considered to be invalid. The {@link ValidationMessage}
* value will be present in the {@link ValidationStatus} of this validator.
*
*
- *
+ *
* You can add multiple rules via the {@link #addRule(ObservableValue, ValidationMessage)} and {@link #addRule(ObservableValue)} method.
* If multiple rules are violated, each message will be present.
*/
public class ObservableRuleBasedValidator implements Validator {
- /*
- These lists are used to store the rules that this validator is working with.
- The reason for this is to prevent problems with garbage collection.
- If the validator wouldn't keep references to all given rules it would be possible that they are
- removed by the garbage collector which would result in wrong validation results.
- To prevent this we store references to all given rules here.
- Don't get confused by the fact that no values are only added to these lists but not obtained.
- */
- private List> booleanRules = new ArrayList<>();
- private List> complexRules = new ArrayList<>();
-
- private ValidationStatus validationStatus = new ValidationStatus();
+ private final List unregisterableRules = new ArrayList<>();
+
+ private final ValidationStatus validationStatus = new ValidationStatus();
/**
* Creates an instance of the Validator without any rules predefined.
*/
- public ObservableRuleBasedValidator() {}
+ public ObservableRuleBasedValidator() {
+ }
/**
* Creates an instance of the Validator with the given rule predefined.
@@ -86,50 +80,49 @@ public ObservableRuleBasedValidator(ObservableValue rule, ValidationMes
* Creates an instance of the Validator with the given complex rules predefined.
* It's a shortcut for creating an empty validator and
* adding one or multiple complex rules with {@link #addRule(ObservableValue)}.
+ *
* @param rules
*/
- public ObservableRuleBasedValidator(ObservableValue ... rules) {
+ public ObservableRuleBasedValidator(ObservableValue... rules) {
for (ObservableValue rule : rules) {
addRule(rule);
}
}
- /**
- * Add a rule for this validator.
- *
- * The rule defines a condition that has to be fulfilled.
- *
- * A rule is defined by an observable boolean value. If the rule has a value of true the rule is
- * "fulfilled". If the rule has a value of false the rule is violated. In this case the given message
- * object will be added to the status of this validator.
- *
- * There are some predefined rules for common use cases in the {@link ObservableRules} class that can be used.
- *
- * @param rule
- * @param message
- */
- public void addRule(ObservableValue rule, ValidationMessage message) {
- booleanRules.add(rule);
-
- rule.addListener((observable, oldValue, newValue) -> {
- validateBooleanRule(newValue, message);
- });
-
- validateBooleanRule(rule.getValue(), message);
- }
-
- private void validateBooleanRule(boolean isValid, ValidationMessage message) {
- if (isValid) {
- validationStatus.removeMessage(message);
- } else {
- validationStatus.addMessage(message);
- }
- }
-
- @Override
- public ValidationStatus getValidationStatus() {
- return validationStatus;
- }
+ /**
+ * Add a rule for this validator.
+ *
+ * The rule defines a condition that has to be fulfilled.
+ *
+ * A rule is defined by an observable boolean value. If the rule has a value of true the rule is
+ * "fulfilled". If the rule has a value of false the rule is violated. In this case the given message
+ * object will be added to the status of this validator.
+ *
+ * There are some predefined rules for common use cases in the {@link ObservableRules} class that can be used.
+ *
+ * @param validProperty
+ * @param message
+ */
+ public void addRule(ObservableValue validProperty, ValidationMessage message) {
+ unregisterableRules.add(
+ FXProperties.runNowAndOnPropertyChange((observable, oldValue, newValue) ->
+ validateBooleanRule(newValue, message)
+ , validProperty)
+ );
+ }
+
+ public void validateBooleanRule(boolean isValid, ValidationMessage message) {
+ if (isValid) {
+ hideMessage(message);
+ } else {
+ showMessage(message);
+ }
+ }
+
+ @Override
+ public ValidationStatus getValidationStatus() {
+ return validationStatus;
+ }
/**
* Add a complex rule for this validator.
@@ -140,26 +133,37 @@ public ValidationStatus getValidationStatus() {
* If the observable doesn't contain a value (in other words it contains null) the rule is considered to
* be fulfilled and the validation status of this validator will be valid (given that no other rule is violated).
*
- *
- * This method allows some more complex rules compared to {@link #addRule(ObservableValue, ValidationMessage)}.
- * This way you can define rules that have different messages for specific cases.
+ *
+ * This method allows some more complex rules compared to {@link #addRule(ObservableValue, ValidationMessage)}.
+ * This way you can define rules that have different messages for specific cases.
*
* @param rule
*/
public void addRule(ObservableValue rule) {
- complexRules.add(rule);
-
- rule.addListener((observable, oldValue, newValue) -> {
- if(oldValue != null) {
- validationStatus.removeMessage(oldValue);
- }
- if(newValue != null) {
- validationStatus.addMessage(newValue);
- }
- });
-
- if(rule.getValue() != null) {
- validationStatus.addMessage(rule.getValue());
+ unregisterableRules.add(
+ FXProperties.runNowAndOnPropertyChange((observable, oldValue, newValue) -> {
+ hideMessage(oldValue); // does nothing if oldValue is null
+ showMessage(newValue); // does nothing if newValue is null
+ }, rule)
+ );
+ }
+
+ private void showMessage(ValidationMessage message) {
+ if (message != null) {
+ validationStatus.addMessage(message);
+ }
+ }
+
+ private void hideMessage(ValidationMessage message) {
+ if (message != null) {
+ validationStatus.removeMessage(message);
}
}
+
+ public void clear() {
+ unregisterableRules.forEach(Unregisterable::unregister);
+ unregisterableRules.clear();
+ validationStatus.clearMessages();
+ }
+
}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java
index 57c3b90e3..9ece55d7b 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationMessage.java
@@ -15,6 +15,8 @@
******************************************************************************/
package dev.webfx.stack.ui.validation.mvvmfx;
+import javafx.beans.value.ObservableStringValue;
+
import java.util.Objects;
/**
@@ -25,26 +27,30 @@
*/
public class ValidationMessage {
- private final String message;
+ private final ObservableStringValue messageProperty;
private final Severity severity;
- public ValidationMessage(Severity severity, String message) {
+ public ValidationMessage(Severity severity, ObservableStringValue messageProperty) {
this.severity = Objects.requireNonNull(severity);
- this.message = Objects.requireNonNull(message);
+ this.messageProperty = Objects.requireNonNull(messageProperty);
}
- public static ValidationMessage warning(String message) {
- return new ValidationMessage(Severity.WARNING, message);
+ public static ValidationMessage warning(ObservableStringValue messageProperty) {
+ return new ValidationMessage(Severity.WARNING, messageProperty);
}
- public static ValidationMessage error(String message) {
- return new ValidationMessage(Severity.ERROR, message);
+ public static ValidationMessage error(ObservableStringValue messageProperty) {
+ return new ValidationMessage(Severity.ERROR, messageProperty);
}
+ public ObservableStringValue messageProperty() {
+ return messageProperty;
+ }
+
public String getMessage() {
- return message;
+ return messageProperty.get();
}
public Severity getSeverity() {
@@ -54,27 +60,27 @@ public Severity getSeverity() {
@Override
public String toString() {
return "ValidationMessage{" +
- "message='" + message + '\'' +
- ", severity=" + severity +
- '}';
+ "messageProperty='" + messageProperty + '\'' +
+ ", severity=" + severity +
+ '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
- if (o == null || !(o instanceof ValidationMessage))
+ if (!(o instanceof ValidationMessage))
return false;
ValidationMessage that = (ValidationMessage) o;
- return message.equals(that.message) && severity == that.severity;
+ return messageProperty.equals(that.messageProperty) && severity == that.severity;
}
@Override
public int hashCode() {
- int result = message.hashCode();
+ int result = messageProperty.hashCode();
result = 31 * result + severity.hashCode();
return result;
}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java
index d1f82602e..82d4062fb 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/ValidationStatus.java
@@ -37,11 +37,11 @@
*/
public class ValidationStatus {
- private ListProperty messages = new SimpleListProperty<>(FXCollections.observableArrayList());
+ private final ListProperty messages = new SimpleListProperty<>(FXCollections.observableArrayList());
- private ObservableList unmodifiableMessages = FXCollections.unmodifiableObservableList(messages);
- private ObservableList errorMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.ERROR));
- private ObservableList warningMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.WARNING));
+ private final ObservableList unmodifiableMessages = FXCollections.unmodifiableObservableList(messages);
+ private final ObservableList errorMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.ERROR));
+ private final ObservableList warningMessages = new FilteredList<>(unmodifiableMessages, message -> message.getSeverity().equals(Severity.WARNING));
/**
* Intended for subclasses used by special validator implementations.
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java
index a3e093bd9..6ede92357 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ControlsFxVisualizer.java
@@ -17,7 +17,7 @@
import dev.webfx.stack.ui.validation.mvvmfx.Severity;
import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage;
-import dev.webfx.stack.ui.validation.controlsfx.validation.ValidationSupport;
+import dev.webfx.stack.ui.validation.controlsfx.validation.ControlsFxValidationSupport;
import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.GraphicValidationDecoration;
import dev.webfx.stack.ui.validation.controlsfx.validation.decoration.ValidationDecoration;
import javafx.scene.control.Control;
@@ -52,7 +52,7 @@ public void setDecoration(ValidationDecoration decoration) {
@Override
void applyRequiredVisualization(Control control, boolean required) {
- ValidationSupport.setRequired(control, required);
+ ControlsFxValidationSupport.setRequired(control, required);
if (required) {
decoration.applyRequiredDecoration(control);
}
@@ -74,12 +74,14 @@ void applyVisualization(Control control, Optional messageOpti
}
} else {
- decoration.removeDecorations(control);
- }
-
- if (required) {
- decoration.applyRequiredDecoration(control);
+ removeDecorations(control);
}
+
+ applyRequiredVisualization(control, required);
+ }
+
+ @Override
+ public void removeDecorations(Control control) {
+ decoration.removeDecorations(control);
}
-
}
diff --git a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java
index 4b6d33d41..447baca68 100644
--- a/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java
+++ b/webfx-stack-ui-validation/src/main/java/dev/webfx/stack/ui/validation/mvvmfx/visualization/ValidationVisualizerBase.java
@@ -15,11 +15,12 @@
******************************************************************************/
package dev.webfx.stack.ui.validation.mvvmfx.visualization;
+import dev.webfx.kit.util.properties.FXProperties;
+import dev.webfx.kit.util.properties.ObservableLists;
+import dev.webfx.platform.uischeduler.UiScheduler;
import dev.webfx.stack.ui.validation.mvvmfx.Severity;
import dev.webfx.stack.ui.validation.mvvmfx.ValidationMessage;
import dev.webfx.stack.ui.validation.mvvmfx.ValidationStatus;
-import javafx.application.Platform;
-import javafx.collections.ListChangeListener;
import javafx.scene.control.Control;
import java.util.Optional;
@@ -42,20 +43,13 @@ public abstract class ValidationVisualizerBase implements ValidationVisualizer {
@Override
public void initVisualization(final ValidationStatus result, final Control control, boolean required) {
if (control.getSkin() == null) {
- control.skinProperty().addListener((observable, oldValue, newValue) -> initVisualization(result, control, required));
+ FXProperties.onPropertySet(control.skinProperty(), skin -> initVisualization(result, control, required));
return;
}
- if (required) {
- applyRequiredVisualization(control, required);
- }
-
- applyVisualization(control, result.getHighestMessage(), required);
-
- result.getMessages().addListener((ListChangeListener) c -> {
- while (c.next()) {
- Platform.runLater(() -> applyVisualization(control, result.getHighestMessage(), required));
- }
- });
+
+ ObservableLists.runNowAndOnListChange(c -> UiScheduler.runInUiThread(() ->
+ applyVisualization(control, result.getHighestMessage(), required)
+ ), result.getMessages());
}
/**
@@ -93,5 +87,7 @@ public void initVisualization(final ValidationStatus result, final Control contr
* a boolean flag indicating whether this control is mandatory or not.
*/
abstract void applyVisualization(Control control, Optional messageOptional, boolean required);
+
+ public abstract void removeDecorations(Control control);
}
diff --git a/webfx-stack-ui-validation/src/main/java/module-info.java b/webfx-stack-ui-validation/src/main/java/module-info.java
index 9aca87547..b37e66d92 100644
--- a/webfx-stack-ui-validation/src/main/java/module-info.java
+++ b/webfx-stack-ui-validation/src/main/java/module-info.java
@@ -6,8 +6,16 @@
requires javafx.base;
requires javafx.controls;
requires javafx.graphics;
+ requires webfx.extras.imagestore;
+ requires webfx.extras.util.background;
+ requires webfx.extras.util.border;
+ requires webfx.extras.util.scene;
+ requires webfx.kit.util;
+ requires webfx.platform.uischeduler;
+ requires webfx.platform.util;
// Exported packages
+ exports dev.webfx.stack.ui.validation;
exports dev.webfx.stack.ui.validation.controlsfx.control.decoration;
exports dev.webfx.stack.ui.validation.controlsfx.impl;
exports dev.webfx.stack.ui.validation.controlsfx.impl.skin;