Make controls more FXML-friendly

This commit is contained in:
mkpaz 2023-05-28 16:40:41 +04:00
parent 2956ef7558
commit 998bd69334
12 changed files with 176 additions and 37 deletions

@ -30,28 +30,75 @@ package atlantafx.base.controls;
import atlantafx.base.util.MaskChar; import atlantafx.base.util.MaskChar;
import atlantafx.base.util.MaskTextFormatter; import atlantafx.base.util.MaskTextFormatter;
import java.util.List; import java.util.List;
import java.util.Objects;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.jetbrains.annotations.Nullable;
/** /**
* This is a convenience wrapper for instantiating a {@link CustomTextField} * This is a convenience wrapper for instantiating a {@link CustomTextField} with a
* with {@code MaskTextFormatter}. For additional info refer to the {@link MaskTextFormatter} * {@code MaskTextFormatter}. For additional info refer to the {@link MaskTextFormatter}
* docs. * docs.
*/ */
public class MaskTextField extends CustomTextField { public class MaskTextField extends CustomTextField {
protected final MaskTextFormatter formatter; /**
* The whole dancing around the editable mask property is solely due to SceneBuilder
* not works without no-arg constructor. It requires to make formatter value mutable
* as well, which is not really tested and never intended to be supported. Also, since
* the formatter property is not bound to the text field formatter property, setting the
* latter manually can lead to memory leak.
*/
protected final StringProperty mask = new SimpleStringProperty(this, "mask");
public MaskTextField(@NamedArg("text") String mask) { protected final ReadOnlyObjectWrapper<MaskTextFormatter> formatter =
new ReadOnlyObjectWrapper<>(this, "formatter");
public MaskTextField() {
super("");
init();
}
public MaskTextField(@NamedArg("mask") String mask) {
this("", mask); this("", mask);
} }
public MaskTextField(@NamedArg("text") String text, @NamedArg("mask") String mask) { private MaskTextField(@NamedArg("text") String text,
super(text); @NamedArg("mask") String mask) {
this.formatter = MaskTextFormatter.create(this, mask); super(Objects.requireNonNullElse(text, ""));
formatter.set(MaskTextFormatter.create(this, mask));
setMask(mask); // set mask only after creating a formatter, for validation
init();
} }
public MaskTextField(String text, List<MaskChar> mask) { public MaskTextField(String text, List<MaskChar> mask) {
super(text); super(Objects.requireNonNullElse(text, ""));
this.formatter = MaskTextFormatter.create(this, mask);
formatter.set(MaskTextFormatter.create(this, mask));
setMask(null);
init();
}
protected void init() {
mask.addListener((obs, old, val) -> {
// this will replace the current text value with placeholder mask,
// so, neither text no prompt won't be shown in the SceneBuilder
formatter.set(val != null ? MaskTextFormatter.create(this, val) : null);
});
}
public StringProperty maskProperty() {
return mask;
}
public @Nullable String getMask() {
return mask.get();
}
public void setMask(@Nullable String mask) {
this.mask.set(mask);
} }
} }

@ -6,6 +6,7 @@ import atlantafx.base.util.Animations;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function; import java.util.function.Function;
import javafx.animation.Animation; import javafx.animation.Animation;
import javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -52,7 +53,7 @@ public class ModalPane extends Control {
* @param topViewOrder the {@link #viewOrderProperty()} value to be set * @param topViewOrder the {@link #viewOrderProperty()} value to be set
* to display the modal pane on top of the parent container. * to display the modal pane on top of the parent container.
*/ */
public ModalPane(int topViewOrder) { public ModalPane(@NamedArg("topViewOrder") int topViewOrder) {
super(); super();
this.topViewOrder = topViewOrder; this.topViewOrder = topViewOrder;
} }

@ -30,58 +30,65 @@ package atlantafx.base.controls;
import atlantafx.base.util.PasswordTextFormatter; import atlantafx.base.util.PasswordTextFormatter;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringProperty;
/** /**
* This is a convenience wrapper for instantiating a {@link CustomTextField} * This is a convenience wrapper for instantiating a {@link CustomTextField}
* with {@code PasswordTextFormatter}. For additional info refer to the {@link PasswordTextFormatter} * with {@code PasswordTextFormatter}. For additional info refer to the
* docs. * {@link PasswordTextFormatter} docs.
*/ */
public class PasswordTextField extends CustomTextField { public class PasswordTextField extends CustomTextField {
protected final PasswordTextFormatter formatter; protected final ReadOnlyObjectWrapper<PasswordTextFormatter> formatter
= new ReadOnlyObjectWrapper<>(this, "formatter");
public PasswordTextField() {
this("", PasswordTextFormatter.BULLET);
}
public PasswordTextField(@NamedArg("text") String text) { public PasswordTextField(@NamedArg("text") String text) {
this(text, PasswordTextFormatter.BULLET); this(text, PasswordTextFormatter.BULLET);
} }
public PasswordTextField(@NamedArg("text") String text, @NamedArg("bullet") char bullet) { protected PasswordTextField(@NamedArg("text") String text,
@NamedArg("bullet") char bullet) {
super(text); super(text);
this.formatter = PasswordTextFormatter.create(this, bullet); formatter.set(PasswordTextFormatter.create(this, bullet));
} }
/** /**
* See {@link PasswordTextFormatter#passwordProperty()}. * See {@link PasswordTextFormatter#passwordProperty()}.
*/ */
public ReadOnlyStringProperty passwordProperty() { public ReadOnlyStringProperty passwordProperty() {
return formatter.passwordProperty(); return formatter.get().passwordProperty();
} }
/** /**
* See {@link PasswordTextFormatter#getPassword()}. * See {@link PasswordTextFormatter#getPassword()}.
*/ */
public String getPassword() { public String getPassword() {
return formatter.getPassword(); return formatter.get().getPassword();
} }
/** /**
* See {@link PasswordTextFormatter#revealPasswordProperty()}. * See {@link PasswordTextFormatter#revealPasswordProperty()}.
*/ */
public BooleanProperty revealPasswordProperty() { public BooleanProperty revealPasswordProperty() {
return formatter.revealPasswordProperty(); return formatter.get().revealPasswordProperty();
} }
/** /**
* See {@link PasswordTextFormatter#isRevealPassword()}. * See {@link PasswordTextFormatter#getRevealPassword()}.
*/ */
public boolean isRevealPassword() { public boolean getRevealPassword() {
return formatter.isRevealPassword(); return formatter.get().getRevealPassword();
} }
/** /**
* See {@link PasswordTextFormatter#setRevealPassword(boolean)}. * See {@link PasswordTextFormatter#setRevealPassword(boolean)}.
*/ */
public void setRevealPassword(boolean reveal) { public void setRevealPassword(boolean reveal) {
formatter.setRevealPassword(reveal); formatter.get().setRevealPassword(reveal);
} }
} }

@ -14,6 +14,7 @@ import javafx.util.StringConverter;
public class RingProgressIndicator extends ProgressIndicator { public class RingProgressIndicator extends ProgressIndicator {
public RingProgressIndicator() { public RingProgressIndicator() {
super();
} }
public RingProgressIndicator(double progress) { public RingProgressIndicator(double progress) {

@ -9,6 +9,8 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -131,7 +133,7 @@ public class ModalBox extends AnchorPane {
// call user specified close handler // call user specified close handler
if (onClose.get() != null) { if (onClose.get() != null) {
onClose.get().run(); onClose.get().handle(new Event(Event.ANY));
} }
} }
@ -145,18 +147,18 @@ public class ModalBox extends AnchorPane {
* handler will be executed after the default close handler. Therefore, you * handler will be executed after the default close handler. Therefore, you
* can use it to perform arbitrary actions on dialog close. * can use it to perform arbitrary actions on dialog close.
*/ */
protected final ObjectProperty<Runnable> onClose = protected final ObjectProperty<EventHandler<? super Event>> onClose =
new SimpleObjectProperty<>(this, "onClose"); new SimpleObjectProperty<>(this, "onClose");
public Runnable getOnClose() { public EventHandler<? super Event> getOnClose() {
return onClose.get(); return onClose.get();
} }
public ObjectProperty<Runnable> onCloseProperty() { public ObjectProperty<EventHandler<? super Event>> onCloseProperty() {
return onClose; return onClose;
} }
public void setOnClose(Runnable onClose) { public void setOnClose(EventHandler<? super Event> onClose) {
this.onClose.set(onClose); this.onClose.set(onClose);
} }

@ -62,11 +62,6 @@ public class MaskTextFormatter extends TextFormatter<String> {
this.filter = filter; this.filter = filter;
} }
public MaskTextFormatter(MaskTextFilter filter, TextField field) {
super(filter);
this.filter = filter;
}
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
// Factory Methods // // Factory Methods //
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////

@ -81,7 +81,7 @@ public class PasswordTextFormatter extends TextFormatter<String> {
/** /**
* See {@link #revealPasswordProperty}. * See {@link #revealPasswordProperty}.
*/ */
public boolean isRevealPassword() { public boolean getRevealPassword() {
return revealPasswordProperty().get(); return revealPasswordProperty().get();
} }

@ -111,10 +111,10 @@ public final class CustomTextFieldPage extends OutlinePage {
var icon = new FontIcon(Feather.EYE_OFF); var icon = new FontIcon(Feather.EYE_OFF);
icon.setCursor(Cursor.HAND); icon.setCursor(Cursor.HAND);
icon.setOnMouseClicked(e -> { icon.setOnMouseClicked(e -> {
icon.setIconCode(tf.isRevealPassword() icon.setIconCode(tf.getRevealPassword()
? Feather.EYE_OFF : Feather.EYE ? Feather.EYE_OFF : Feather.EYE
); );
tf.setRevealPassword(!tf.isRevealPassword()); tf.setRevealPassword(!tf.getRevealPassword());
}); });
tf.setRight(icon); tf.setRight(icon);
//snippet_3:end //snippet_3:end

@ -94,7 +94,7 @@ public final class ThemePage extends OutlinePage {
sceneBuilderDialog = new Lazy<>(() -> { sceneBuilderDialog = new Lazy<>(() -> {
var dialog = new SceneBuilderDialog(); var dialog = new SceneBuilderDialog();
dialog.setClearOnClose(true); dialog.setClearOnClose(true);
dialog.setOnClose(dialog::reset); dialog.setOnClose(e -> dialog.reset());
return dialog; return dialog;
}); });

@ -4,13 +4,18 @@ package atlantafx.sampler.page.showcase;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED; import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
import atlantafx.base.controls.MaskTextField;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.util.NodeUtils; import atlantafx.sampler.util.NodeUtils;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
@ -79,4 +84,15 @@ public final class OverviewPage extends ScrollPane implements Page {
@Override @Override
public void reset() { public void reset() {
} }
///////////////////////////////////////////////////////////////////////////
public static class Controller implements Initializable {
public @FXML MaskTextField phoneTf;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
phoneTf.setText("(415) 273-91-64");
}
}
} }

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import atlantafx.base.controls.Breadcrumbs?>
<?import atlantafx.base.controls.Calendar?>
<?import atlantafx.base.controls.Card?>
<?import atlantafx.base.controls.CustomTextField?>
<?import atlantafx.base.controls.PasswordTextField?>
<?import atlantafx.base.controls.RingProgressIndicator?>
<?import atlantafx.base.controls.Spacer?>
<?import atlantafx.base.controls.Tile?>
<?import atlantafx.base.controls.ToggleSwitch?>
<?import atlantafx.base.layout.DeckPane?>
<?import atlantafx.base.layout.InputGroup?>
<?import atlantafx.base.layout.ModalBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="897.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Label layoutX="14.0" layoutY="14.0" style="-fx-text-fill: green;" text="Breadcrumbs" />
<Breadcrumbs layoutX="14.0" layoutY="45.0" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="45.0" prefWidth="280.0" style="-fx-border-color: red;" />
<Calendar layoutX="14.0" layoutY="107.0" />
<CustomTextField layoutX="14.0" layoutY="470.0" prefHeight="36.0" prefWidth="264.0" text="Text" />
<Label layoutX="14.0" layoutY="442.0" style="-fx-text-fill: green;" text="CustomTextField" />
<ToggleSwitch layoutX="411.0" layoutY="33.0" />
<Separator layoutX="295.0" layoutY="15.0" orientation="VERTICAL" prefHeight="759.0" prefWidth="25.0" />
<PasswordTextField layoutX="14.0" layoutY="546.0" prefHeight="36.0" prefWidth="264.0" text="password" />
<Label layoutX="14.0" layoutY="517.0" style="-fx-text-fill: green;" text="PasswordTextField" />
<RingProgressIndicator layoutX="326.0" layoutY="25.0" progress="0.35" />
<Tile layoutX="320.0" layoutY="209.0" prefHeight="73.0" prefWidth="483.0" subTitle="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fermentum, quam eu pretium euismod, ipsum mauris interdum massa, at scelerisque nulla augue a nunc. Vivamus vehicula rhoncus est, ut placerat nulla pellentesque vel. Duis ac mattis sapien. " title="Title" />
<InputGroup layoutX="526.0" layoutY="57.0">
<children>
<ToggleButton mnemonicParsing="false" text="Toggle1" />
<ToggleButton mnemonicParsing="false" text="Toggle2" />
<ToggleButton layoutX="124.0" layoutY="10.0" mnemonicParsing="false" text="Toggle3" />
</children>
</InputGroup>
<Label layoutX="527.0" layoutY="23.0" style="-fx-text-fill: green;" text="InputGroup" />
<HBox alignment="CENTER_LEFT" layoutX="527.0" layoutY="137.0" prefHeight="56.0" prefWidth="280.0" style="-fx-border-color: red;">
<children>
<Label text="Left" />
<Spacer prefHeight="73.0" prefWidth="219.0" />
<Label layoutX="10.0" layoutY="10.0" text="Right" />
</children>
</HBox>
<Label layoutX="526.0" layoutY="108.0" style="-fx-text-fill: green;" text="Spacer" />
<VBox layoutX="863.0" layoutY="128.0" prefHeight="200.0" prefWidth="100.0" style="-fx-border-color: red;">
<children>
<Label text="Top" />
<Spacer />
<Label layoutX="10.0" layoutY="10.0" text="Bottom" />
</children>
</VBox>
<Label layoutX="336.0" layoutY="183.0" style="-fx-text-fill: green;" text="Tile" />
<ModalBox layoutX="326.0" layoutY="459.0" prefHeight="175.0" prefWidth="264.0" style="-fx-border-color: red;" />
<Label layoutX="325.0" layoutY="432.0" style="-fx-text-fill: green;" text="ModalBox" />
<Card layoutX="641.0" layoutY="457.0" prefHeight="229.0" prefWidth="309.0" styleClass="elevated-2" />
<Label layoutX="642.0" layoutY="432.0" style="-fx-text-fill: green;" text="Card" />
<DeckPane layoutX="326.0" layoutY="695.0" prefHeight="127.0" prefWidth="264.0" style="-fx-border-color: red;" />
<Label layoutX="325.0" layoutY="665.0" style="-fx-text-fill: green;" text="DeckPane" />
</children>
</AnchorPane>

@ -2,6 +2,7 @@
<?import atlantafx.base.controls.Calendar?> <?import atlantafx.base.controls.Calendar?>
<?import atlantafx.base.controls.CustomTextField?> <?import atlantafx.base.controls.CustomTextField?>
<?import atlantafx.base.controls.MaskTextField?>
<?import atlantafx.base.controls.PasswordTextField?> <?import atlantafx.base.controls.PasswordTextField?>
<?import atlantafx.base.controls.RingProgressIndicator?> <?import atlantafx.base.controls.RingProgressIndicator?>
<?import atlantafx.base.controls.Tile?> <?import atlantafx.base.controls.Tile?>
@ -52,7 +53,7 @@
<?import javafx.scene.paint.Color?> <?import javafx.scene.paint.Color?>
<?import org.kordamp.ikonli.javafx.FontIcon?> <?import org.kordamp.ikonli.javafx.FontIcon?>
<GridPane hgap="40.0" vgap="20.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1"> <GridPane hgap="40.0" vgap="20.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="atlantafx.sampler.page.showcase.OverviewPage$Controller">
<columnConstraints> <columnConstraints>
<ColumnConstraints hgrow="NEVER" /> <ColumnConstraints hgrow="NEVER" />
<ColumnConstraints hgrow="NEVER" /> <ColumnConstraints hgrow="NEVER" />
@ -208,6 +209,7 @@
<RowConstraints vgrow="NEVER" /> <RowConstraints vgrow="NEVER" />
<RowConstraints vgrow="NEVER" /> <RowConstraints vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" /> <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
</rowConstraints> </rowConstraints>
<children> <children>
<TextField promptText="Prompt" GridPane.columnIndex="1" /> <TextField promptText="Prompt" GridPane.columnIndex="1" />
@ -223,6 +225,7 @@
<PasswordTextField text="password" GridPane.rowIndex="2" /> <PasswordTextField text="password" GridPane.rowIndex="2" />
<TextField editable="false" promptText="Readonly" GridPane.rowIndex="1" /> <TextField editable="false" promptText="Readonly" GridPane.rowIndex="1" />
<TextField disable="true" promptText="Disabled" GridPane.columnIndex="1" GridPane.rowIndex="1" /> <TextField disable="true" promptText="Disabled" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<MaskTextField fx:id="phoneTf" mask="(NNN) NNN-NN-NN" GridPane.rowIndex="3" />
</children> </children>
</GridPane> </GridPane>
<GridPane hgap="10.0" layoutX="30.0" layoutY="30.0" vgap="10.0" GridPane.rowIndex="2"> <GridPane hgap="10.0" layoutX="30.0" layoutY="30.0" vgap="10.0" GridPane.rowIndex="2">