Improve control and widget samples

This commit is contained in:
mkpaz 2022-10-04 16:34:44 +04:00
parent 1c4c6a5232
commit cc3cd4a391
15 changed files with 515 additions and 71 deletions

@ -137,6 +137,7 @@ public class RingProgressIndicatorSkin extends SkinBase<RingProgressIndicator> {
protected void toggleIndeterminate() { protected void toggleIndeterminate() {
var indeterminate = getSkinnable().isIndeterminate(); var indeterminate = getSkinnable().isIndeterminate();
progressLabel.setManaged(!indeterminate); progressLabel.setManaged(!indeterminate);
progressLabel.setVisible(!indeterminate);
if (indeterminate) { if (indeterminate) {
if (getSkinnable().isVisible()) { if (getSkinnable().isVisible()) {

@ -75,6 +75,7 @@ public final class Styles {
public static final String TEXT_OBLIQUE = "text-oblique"; public static final String TEXT_OBLIQUE = "text-oblique";
public static final String TEXT_STRIKETHROUGH = "text-strikethrough"; public static final String TEXT_STRIKETHROUGH = "text-strikethrough";
public static final String TEXT_UNDERLINED = "text-underlined"; public static final String TEXT_UNDERLINED = "text-underlined";
public static final String TEXT_MUTED = "text-muted";
// @formatter:on // @formatter:on

@ -0,0 +1,158 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.util;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.StringConverter;
import java.util.Objects;
import java.util.function.UnaryOperator;
/**
* An alternative for the {@link javafx.scene.control.PasswordField}.
* The formatter (un)masks text field content based on boolean property.
*/
public class PasswordTextFormatter extends TextFormatter<String> {
public static final char BULLET = '\u2731'; // heavy asterisk
protected PasswordTextFormatter(StringConverter<String> valueConverter,
UnaryOperator<Change> filter,
TextField textField,
char bullet) {
super(valueConverter, null, filter);
Objects.requireNonNull(valueConverter);
Objects.requireNonNull(filter);
Objects.requireNonNull(textField);
PasswordFilter passwordFilter = (PasswordFilter) getFilter();
passwordFilter.setBullet(bullet);
passwordFilter.setInitialText(textField.getText());
revealPasswordProperty().addListener((obs, old, val) -> {
// Force text field update, because converter is only called on focus
// events by default. Don't use commitValue() here because caret position
// won't be correct due to #javafx-bug (https://bugs.openjdk.org/browse/JDK-8248914).
if (val == null) { return; }
textField.setText(passwordProperty().get());
});
// force text field update on scene show
Platform.runLater(textField::commitValue);
}
public ReadOnlyStringProperty passwordProperty() {
return ((PasswordFilter) getFilter()).password.getReadOnlyProperty();
}
public String getPassword() {
return passwordProperty().get();
}
public BooleanProperty revealPasswordProperty() {
return ((PasswordFilter) getFilter()).revealPassword;
}
public boolean isRevealPassword() {
return revealPasswordProperty().get();
}
public void setRevealPassword(boolean reveal) {
revealPasswordProperty().set(reveal);
}
// Life would be easier if TextFormatter had the default constructor.
public static PasswordTextFormatter create(TextField textField, char bullet) {
var filter = new PasswordFilter();
var converter = new PasswordStringConverter(filter);
return new PasswordTextFormatter(converter, filter, textField, bullet);
}
public static PasswordTextFormatter create(TextField textField) {
return create(textField, BULLET);
}
///////////////////////////////////////////////////////////////////////////
protected static class PasswordStringConverter extends StringConverter<String> {
protected final PasswordFilter filter;
public PasswordStringConverter(PasswordFilter filter) {
this.filter = filter;
}
@Override
public String toString(String s) {
if (s == null) { return ""; }
return filter.revealPassword.get() ? filter.password.get() : filter.maskText(s.length());
}
@Override
public String fromString(String string) {
return filter.password.get();
}
}
protected static class PasswordFilter implements UnaryOperator<Change> {
protected final ReadOnlyStringWrapper password = new ReadOnlyStringWrapper("");
protected final BooleanProperty revealPassword = new SimpleBooleanProperty(false);
protected final StringBuilder sb = new StringBuilder();
protected char bullet = PasswordTextFormatter.BULLET;
@Override
public TextFormatter.Change apply(TextFormatter.Change change) {
// Since we are using setText() to force text field to update (see above),
// we should protect internal password value from changing when `revealPassword`is toggled.
if (Objects.equals(change.getText(), sb.toString())) {
if (!revealPassword.get()) {
change.setText(maskText(change.getText().length()));
}
return change;
}
if (change.isReplaced()) {
sb.replace(change.getRangeStart(), change.getRangeEnd(), change.getText());
} else if (change.isDeleted()) {
sb.delete(change.getRangeStart(), change.getRangeEnd());
} else if (change.isAdded()) {
if (change.getRangeStart() == sb.length()) {
sb.append(change.getText());
} else {
sb.insert(change.getRangeStart(), change.getText());
}
}
// mask new text, so it won't appear on user input
if (change.getText() != null && !change.getText().isEmpty() && !revealPassword.get()) {
change.setText(maskText(change.getText().length()));
}
password.set(sb.toString());
return change;
}
protected void setBullet(char bullet) {
this.bullet = bullet;
}
protected String maskText(int length) {
return String.valueOf(bullet).repeat(length);
}
protected void setInitialText(String text) {
if (text != null && !text.isEmpty()) {
sb.append(text);
password.set(sb.toString());
}
}
}
}

@ -119,11 +119,11 @@ class MainLayer extends BorderPane {
// animate switching between pages // animate switching between pages
subLayerPane.getChildren().add(nextPage.getView()); subLayerPane.getChildren().add(nextPage.getView());
subLayerPane.getChildren().remove(prevPage.getView());
var transition = new FadeTransition(Duration.millis(PAGE_TRANSITION_DURATION), nextPage.getView()); var transition = new FadeTransition(Duration.millis(PAGE_TRANSITION_DURATION), nextPage.getView());
transition.setFromValue(0.0); transition.setFromValue(0.0);
transition.setToValue(1.0); transition.setToValue(1.0);
transition.setOnFinished(t -> { transition.setOnFinished(t -> {
subLayerPane.getChildren().remove(prevPage.getView());
if (nextPage instanceof Pane nextPane) { nextPane.toFront(); } if (nextPage instanceof Pane nextPane) { nextPane.toFront(); }
}); });
transition.play(); transition.play();

@ -2,8 +2,10 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.base.controls.CustomTextField; import atlantafx.base.controls.CustomTextField;
import atlantafx.base.util.PasswordTextFormatter;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import javafx.scene.Cursor;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@ -27,47 +29,71 @@ public class CustomTextFieldPage extends AbstractPage {
rightIconSample(), rightIconSample(),
bothIconsSample(), bothIconsSample(),
successSample(), successSample(),
dangerSample() dangerSample(),
passwordSample()
)); ));
} }
private SampleBlock leftIconSample() { private SampleBlock leftIconSample() {
var leftIconField = new CustomTextField(); var tf = new CustomTextField();
leftIconField.setPromptText("Prompt text"); tf.setPromptText("Prompt text");
leftIconField.setRight(new FontIcon(Feather.X)); tf.setRight(new FontIcon(Feather.X));
leftIconField.setPrefWidth(PREF_WIDTH); tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Left", leftIconField); return new SampleBlock("Left", tf);
} }
private SampleBlock rightIconSample() { private SampleBlock rightIconSample() {
var rightIconField = new CustomTextField(); var tf = new CustomTextField();
rightIconField.setPromptText("Prompt text"); tf.setPromptText("Prompt text");
rightIconField.setLeft(new FontIcon(Feather.MAP_PIN)); tf.setLeft(new FontIcon(Feather.MAP_PIN));
rightIconField.setPrefWidth(PREF_WIDTH); tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Right", rightIconField); return new SampleBlock("Right", tf);
} }
private SampleBlock bothIconsSample() { private SampleBlock bothIconsSample() {
var bothIconField = new CustomTextField("Text"); var tf = new CustomTextField("Text");
bothIconField.setLeft(new FontIcon(Feather.MAP_PIN)); tf.setLeft(new FontIcon(Feather.MAP_PIN));
bothIconField.setRight(new FontIcon(Feather.X)); tf.setRight(new FontIcon(Feather.X));
bothIconField.setPrefWidth(PREF_WIDTH); tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Both Sides", bothIconField); return new SampleBlock("Both Sides", tf);
} }
private SampleBlock successSample() { private SampleBlock successSample() {
var successField = new CustomTextField("Text"); var tf = new CustomTextField("Text");
successField.pseudoClassStateChanged(STATE_SUCCESS, true); tf.pseudoClassStateChanged(STATE_SUCCESS, true);
successField.setRight(new FontIcon(Feather.X)); tf.setRight(new FontIcon(Feather.X));
successField.setPrefWidth(PREF_WIDTH); tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Success", successField); return new SampleBlock("Success", tf);
} }
private SampleBlock dangerSample() { private SampleBlock dangerSample() {
var dangerField = new CustomTextField("Text"); var tf = new CustomTextField();
dangerField.pseudoClassStateChanged(STATE_DANGER, true); tf.pseudoClassStateChanged(STATE_DANGER, true);
dangerField.setLeft(new FontIcon(Feather.MAP_PIN)); tf.setLeft(new FontIcon(Feather.MAP_PIN));
dangerField.setPrefWidth(PREF_WIDTH); tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Danger", dangerField); return new SampleBlock("Danger", tf);
}
private SampleBlock passwordSample() {
var tf = new CustomTextField("qwerty");
tf.setPrefWidth(PREF_WIDTH);
var passwordFormatter = PasswordTextFormatter.create(tf);
tf.setTextFormatter(passwordFormatter);
var icon = new FontIcon(Feather.EYE_OFF);
icon.setCursor(Cursor.HAND);
icon.setOnMouseClicked(e -> {
if (passwordFormatter.revealPasswordProperty().get()) {
passwordFormatter.revealPasswordProperty().set(false);
icon.setIconCode(Feather.EYE_OFF);
} else {
passwordFormatter.revealPasswordProperty().set(true);
icon.setIconCode(Feather.EYE);
}
});
tf.setRight(icon);
return new SampleBlock("Password", tf);
} }
} }

@ -1,11 +1,16 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.scene.web.HTMLEditor; import javafx.scene.web.HTMLEditor;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
public class HTMLEditorPage extends AbstractPage { public class HTMLEditorPage extends AbstractPage {
public static final String NAME = "HTMLEditor"; public static final String NAME = "HTMLEditor";
@ -21,9 +26,16 @@ public class HTMLEditorPage extends AbstractPage {
} }
private SampleBlock editorSample() { private SampleBlock editorSample() {
var description = new Text(
"HTMLEditor toolbar buttons use images from 'com/sun/javafx/scene/control/skin/modena'. " +
"In opposite, since AtlantaFX themes are also distributed as single CSS files, it contains no images. " +
"Unfortunately reusing Modena resources isn't possible, because the package isn't opened in OpenJFX 'module-info'."
);
description.getStyleClass().addAll(Styles.TEXT, Styles.WARNING);
var editor = new HTMLEditor(); var editor = new HTMLEditor();
editor.setPrefHeight(400); editor.setPrefHeight(400);
editor.setHtmlText(String.join("<br/><br/>", FAKER.lorem().paragraphs(10))); editor.setHtmlText(String.join("<br/><br/>", FAKER.lorem().paragraphs(10)));
return new SampleBlock("Playground", editor); return new SampleBlock("Playground", new VBox(BLOCK_VGAP, new TextFlow(description), editor));
} }
} }

@ -8,13 +8,18 @@ import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment; import atlantafx.sampler.theme.CSSFragment;
import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.time.LocalDate; import java.time.LocalDate;
@ -31,7 +36,7 @@ public class PopoverPage extends AbstractPage {
public PopoverPage() { public PopoverPage() {
super(); super();
setUserContent(new VBox(Page.PAGE_VGAP, setUserContent(new VBox(Page.PAGE_VGAP,
new HBox(PAGE_HGAP, textSample(), datePickerSample()), new HBox(PAGE_HGAP, textSample(), datePickerSample(), dialogSample()),
positionSample() positionSample()
)); ));
} }
@ -72,6 +77,42 @@ public class PopoverPage extends AbstractPage {
return new SampleBlock("Date Picker", link); return new SampleBlock("Date Picker", link);
} }
private SampleBlock dialogSample() {
var root = new VBox(BLOCK_VGAP);
var popover = new Popover(root);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
var icon = new FontIcon(Feather.ALERT_TRIANGLE);
icon.setIconSize(32); // not always works
icon.setStyle("-fx-icon-size:32px;-fx-icon-color:-color-warning-fg;-fx-fill:-color-warning-fg;" + icon.getStyle());
var label = new Label(FAKER.chuckNorris().fact(), icon);
label.setStyle("-fx-graphic-text-gap:10;");
label.setWrapText(true);
label.setMaxWidth(300);
label.setMaxHeight(Double.MAX_VALUE);
root.getChildren().add(label);
var yesBtn = new Button("Yes");
yesBtn.setDefaultButton(true);
yesBtn.setOnAction(e -> popover.hide());
var noBtn = new Button("No");
noBtn.setOnAction(e -> popover.hide());
var box = new HBox(10, yesBtn, noBtn);
box.setPadding(new Insets(0, 0, 0, 42));
box.setAlignment(Pos.CENTER_LEFT);
root.getChildren().add(box);
var link = createHyperlink("Click me");
link.setOnAction(e -> popover.show(link));
return new SampleBlock("Dialog", link);
}
private SampleBlock positionSample() { private SampleBlock positionSample() {
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(BLOCK_HGAP); grid.setHgap(BLOCK_HGAP);

@ -6,13 +6,13 @@ import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment; import atlantafx.sampler.theme.CSSFragment;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.geometry.VPos;
import javafx.scene.control.Label; import javafx.scene.control.*;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.util.StringConverter; import javafx.util.StringConverter;
@ -24,7 +24,7 @@ import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
// #javafx-bug Indeterminate (animated) progress bar and also progress indicator // #javafx-bug Indeterminate (animated) progress bar and also progress indicator
// are VERY resource expensive. It consumes a single CPU core and a lot of memory. // are very resource expensive. It consumes a single CPU core and a lot of memory.
public class ProgressPage extends AbstractPage { public class ProgressPage extends AbstractPage {
public static final String NAME = "Progress"; public static final String NAME = "Progress";
@ -45,27 +45,46 @@ public class ProgressPage extends AbstractPage {
grid.add(ringIndicatorSample(), 0, 1); grid.add(ringIndicatorSample(), 0, 1);
grid.add(barSizeSample(), 1, 1); grid.add(barSizeSample(), 1, 1);
grid.add(colorChangeSample(), 0, 2); grid.add(indeterminateSample(), 0, 2);
grid.add(colorChangeSample(), 1, 2);
setUserContent(grid); setUserContent(grid);
} }
private SampleBlock basicBarSample() { private SampleBlock basicBarSample() {
var flowPane = new FlowPane(BLOCK_HGAP, BLOCK_VGAP, createBar(0, false), createBar(0.5, false), createBar(1, false), createBar(0.5, true)); var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
createBar(0, false),
createBar(0.5, false),
createBar(1, false),
createBar(0.5, true)
);
flowPane.setAlignment(Pos.CENTER_LEFT); flowPane.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Progress Bar", flowPane); return new SampleBlock("Progress Bar", flowPane);
} }
private SampleBlock basicIndicatorSample() { private SampleBlock basicIndicatorSample() {
var flowPane = new FlowPane(BLOCK_HGAP, BLOCK_VGAP, createIndicator(0, false), createIndicator(0.5, false), createIndicator(1, false), createIndicator(0.5, true)); var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
createIndicator(0, false),
createIndicator(0.5, false),
createIndicator(1, false),
createIndicator(0.5, true)
);
flowPane.setAlignment(Pos.TOP_LEFT); flowPane.setAlignment(Pos.TOP_LEFT);
return new SampleBlock("Progress Indicator", flowPane); return new SampleBlock("Progress Indicator", flowPane);
} }
private SampleBlock barSizeSample() { private SampleBlock barSizeSample() {
var container = new VBox(BLOCK_VGAP, new HBox(20, createBar(0.5, false, SMALL), new Text("small")), new HBox(20, createBar(0.5, false, MEDIUM), new Text("medium")), new HBox(20, createBar(0.5, false, LARGE), new Text("large"))); var container = new VBox(
BLOCK_VGAP,
new HBox(20, createBar(0.5, false, SMALL), new Text("small")),
new HBox(20, createBar(0.5, false, MEDIUM), new Text("medium")),
new HBox(20, createBar(0.5, false, LARGE), new Text("large"))
);
container.setAlignment(Pos.TOP_LEFT);
container.getChildren().forEach(c -> ((HBox) c).setAlignment(Pos.CENTER_LEFT)); container.getChildren().forEach(c -> ((HBox) c).setAlignment(Pos.CENTER_LEFT));
return new SampleBlock("Size", container); return new SampleBlock("Size", container);
@ -75,7 +94,7 @@ public class ProgressPage extends AbstractPage {
var basicIndicator = new RingProgressIndicator(0, false); var basicIndicator = new RingProgressIndicator(0, false);
var customTextIndicator = new RingProgressIndicator(0.5, false); var customTextIndicator = new RingProgressIndicator(0.5, false);
customTextIndicator.setPrefSize(75, 75); customTextIndicator.setMinSize(75, 75);
customTextIndicator.setStringConverter(new StringConverter<>() { customTextIndicator.setStringConverter(new StringConverter<>() {
@Override @Override
public String toString(Double progress) { public String toString(Double progress) {
@ -89,7 +108,7 @@ public class ProgressPage extends AbstractPage {
}); });
var reverseIndicator = new RingProgressIndicator(0.25, true); var reverseIndicator = new RingProgressIndicator(0.25, true);
reverseIndicator.setPrefSize(150, 150); reverseIndicator.setMinSize(150, 150);
var reverseIndicatorLabel = new Label("25%"); var reverseIndicatorLabel = new Label("25%");
reverseIndicatorLabel.getStyleClass().add(TITLE_4); reverseIndicatorLabel.getStyleClass().add(TITLE_4);
@ -139,6 +158,60 @@ public class ProgressPage extends AbstractPage {
return new SampleBlock("Ring Indicator", box); return new SampleBlock("Ring Indicator", box);
} }
private SampleBlock indeterminateSample() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(BLOCK_VGAP);
grid.getColumnConstraints().setAll(
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true)
);
grid.getRowConstraints().setAll(
new RowConstraints(-1, -1, -1, Priority.ALWAYS, VPos.CENTER, true),
new RowConstraints(-1, -1, -1, Priority.NEVER, VPos.CENTER, true)
);
var barToggle = new ToggleButton("Start");
barToggle.textProperty().bind(Bindings.createStringBinding(
() -> barToggle.isSelected() ? "Stop" : "Start", barToggle.selectedProperty())
);
var bar = createBar(0, false);
bar.progressProperty().bind(Bindings.createDoubleBinding(
() -> barToggle.isSelected() ? -1d : 0d, barToggle.selectedProperty())
);
grid.add(bar, 0, 0);
grid.add(barToggle, 0, 1);
var indicatorToggle = new ToggleButton("Start");
indicatorToggle.textProperty().bind(Bindings.createStringBinding(
() -> indicatorToggle.isSelected() ? "Stop" : "Start", indicatorToggle.selectedProperty())
);
var indicator = createIndicator(0, false);
indicator.setPrefSize(75, 75);
indicator.progressProperty().bind(Bindings.createDoubleBinding(
() -> indicatorToggle.isSelected() ? -1d : 0d, indicatorToggle.selectedProperty())
);
grid.add(indicator, 1, 0);
grid.add(indicatorToggle, 1, 1);
var ringToggle = new ToggleButton("Start");
ringToggle.textProperty().bind(Bindings.createStringBinding(
() -> ringToggle.isSelected() ? "Stop" : "Start", ringToggle.selectedProperty())
);
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(75, 75);
ring.progressProperty().bind(Bindings.createDoubleBinding(
() -> ringToggle.isSelected() ? -1d : 0d, ringToggle.selectedProperty())
);
grid.add(ring, 2, 0);
grid.add(ringToggle, 2, 1);
return new SampleBlock("Indeterminate", grid,
"Animated JavaFX progress indicators aren't cheap. They can consume quite a lot of CPU time."
);
}
private SampleBlock colorChangeSample() { private SampleBlock colorChangeSample() {
var stateSuccess = PseudoClass.getPseudoClass("state-success"); var stateSuccess = PseudoClass.getPseudoClass("state-success");
var stateDanger = PseudoClass.getPseudoClass("state-danger"); var stateDanger = PseudoClass.getPseudoClass("state-danger");
@ -164,6 +237,7 @@ public class ProgressPage extends AbstractPage {
var content = new VBox(BLOCK_VGAP); var content = new VBox(BLOCK_VGAP);
content.getChildren().setAll(barStack, runBtn); content.getChildren().setAll(barStack, runBtn);
content.setAlignment(Pos.CENTER_LEFT); content.setAlignment(Pos.CENTER_LEFT);
content.setPrefHeight(200);
bar.progressProperty().addListener((obs, old, val) -> { bar.progressProperty().addListener((obs, old, val) -> {
if (val == null) { return; } if (val == null) { return; }
@ -176,17 +250,17 @@ public class ProgressPage extends AbstractPage {
}); });
new CSSFragment(""" new CSSFragment("""
.example:state-success .progress-bar { .example:state-success .progress-bar {
-color-progress-bar-fill: -color-success-emphasis; -color-progress-bar-fill: -color-success-emphasis;
} }
.example:state-danger .progress-bar { .example:state-danger .progress-bar {
-color-progress-bar-fill: -color-danger-emphasis; -color-progress-bar-fill: -color-danger-emphasis;
} }
.example:state-success .label, .example:state-success .label,
.example:state-danger .label { .example:state-danger .label {
-fx-text-fill: -color-fg-emphasis; -fx-text-fill: -color-fg-emphasis;
} }
""").addTo(content); """).addTo(content);
runBtn.setOnAction(e1 -> { runBtn.setOnAction(e1 -> {
var task = new Task<Void>() { var task = new Task<Void>() {
@ -224,7 +298,7 @@ public class ProgressPage extends AbstractPage {
return new SampleBlock("Dynamic Color Change", content); return new SampleBlock("Dynamic Color Change", content);
} }
private ProgressIndicator createBar(double progress, boolean disabled, String... styleClasses) { private ProgressBar createBar(double progress, boolean disabled, String... styleClasses) {
var bar = new ProgressBar(progress); var bar = new ProgressBar(progress);
bar.getStyleClass().addAll(styleClasses); bar.getStyleClass().addAll(styleClasses);
bar.setDisable(disabled); bar.setDisable(disabled);

@ -7,15 +7,19 @@ import atlantafx.sampler.theme.CSSFragment;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader; import javafx.scene.image.PixelReader;
import javafx.scene.image.WritableImage; import javafx.scene.image.WritableImage;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
import net.datafaker.Faker; import net.datafaker.Faker;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import static atlantafx.sampler.page.Page.PAGE_HGAP; import static atlantafx.sampler.page.Page.PAGE_HGAP;
import static atlantafx.sampler.page.Page.PAGE_VGAP; import static atlantafx.sampler.page.Page.PAGE_VGAP;
@ -43,7 +47,8 @@ public class CardSample extends HBox {
new VBox( new VBox(
PAGE_VGAP, PAGE_VGAP,
imageTextCard(), imageTextCard(),
titleImageCard() titleImageCard(),
statisticCard()
) )
); );
} }
@ -111,7 +116,7 @@ public class CardSample extends HBox {
card.setImage(new ImageView(cropImage)); card.setImage(new ImageView(cropImage));
card.setTitle("Title"); card.setTitle("Title");
var text = new Text(FAKER.lorem().paragraph()); var text = new Text(FAKER.lorem().sentence());
card.setBody(new TextFlow(text)); card.setBody(new TextFlow(text));
return card; return card;
@ -141,4 +146,34 @@ public class CardSample extends HBox {
return card; return card;
} }
private Card statisticCard() {
var card = new Card();
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
card.setTitle("Statistic");
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(10);
card.setBody(grid);
var leftHead = new Text("Active");
leftHead.getStyleClass().add(Styles.TEXT_MUTED);
grid.add(leftHead, 0, 0);
var leftData = new Label("12.87", new FontIcon(Material2AL.ARROW_UPWARD));
leftData.getStyleClass().addAll(Styles.SUCCESS, Styles.TITLE_2);
grid.add(leftData, 0, 1);
var rightHead = new Text("Idle");
rightHead.getStyleClass().add(Styles.TEXT_MUTED);
grid.add(rightHead, 1, 0);
var rightData = new Label("3.74", new FontIcon(Material2AL.ARROW_DOWNWARD));
rightData.getStyleClass().addAll(Styles.DANGER, Styles.TITLE_2);
grid.add(rightData, 1, 1);
return card;
}
} }

@ -56,7 +56,7 @@ public class Stepper extends HBox {
-fx-icon-color: -color-stepper-fg; -fx-icon-color: -color-stepper-fg;
} }
.stepper > .item:selected > .graphic { .stepper > .item:selected > .graphic {
-color-stepper-bg: -color-accent-subtle; -color-stepper-bg: -color-bg-default;
-color-stepper-fg: -color-accent-fg; -color-stepper-fg: -color-accent-fg;
-color-stepper-border: -color-accent-emphasis; -color-stepper-border: -color-accent-emphasis;
} }

@ -7,6 +7,7 @@ import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.page.showcase.widget.Stepper.Item; import atlantafx.sampler.page.showcase.widget.Stepper.Item;
import atlantafx.sampler.theme.CSSFragment; import atlantafx.sampler.theme.CSSFragment;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
@ -61,20 +62,24 @@ public class StepperSample extends SampleBlock {
// == CONTROLS == // == CONTROLS ==
var saveBtn = new Button("Save"); var nextBtn = new Button("Next");
saveBtn.setDefaultButton(true); nextBtn.setDefaultButton(true);
saveBtn.setOnAction(e -> { nextBtn.setOnAction(e -> {
// you can validate user input before moving forward here // you can validate user input before moving forward here
stepper.getSelectedItem().setCompleted(true); stepper.getSelectedItem().setCompleted(true);
stepper.forward(); stepper.forward();
}); });
nextBtn.textProperty().bind(Bindings.createStringBinding(
() -> stepper.canGoForwardProperty().get() ? "Next" : "Done", stepper.canGoForwardProperty())
);
var cancelBtn = new Button("Cancel"); var prevBtn = new Button("Previous");
cancelBtn.getStyleClass().addAll(Styles.FLAT); prevBtn.getStyleClass().addAll(Styles.FLAT);
cancelBtn.setOnAction(e -> { prevBtn.setOnAction(e -> {
stepper.getSelectedItem().setCompleted(false); stepper.getSelectedItem().setCompleted(false);
stepper.backward(); stepper.backward();
}); });
prevBtn.disableProperty().bind(stepper.canGoBackProperty().not());
var iconToggle = new ToggleSwitch("Icons"); var iconToggle = new ToggleSwitch("Icons");
iconToggle.selectedProperty().addListener((obs, old, val) -> { iconToggle.selectedProperty().addListener((obs, old, val) -> {
@ -101,8 +106,8 @@ public class StepperSample extends SampleBlock {
var controls = new HBox( var controls = new HBox(
BLOCK_HGAP, BLOCK_HGAP,
saveBtn, nextBtn,
cancelBtn, prevBtn,
new Spacer(), new Spacer(),
iconToggle, iconToggle,
rotateBtn rotateBtn

@ -3,6 +3,9 @@ package atlantafx.sampler.page.showcase.widget;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import static javafx.scene.control.ContentDisplay.RIGHT;
public class Tag extends Button { public class Tag extends Button {
@ -10,6 +13,7 @@ public class Tag extends Button {
.tag { .tag {
-fx-padding: 4px 6px 4px 6px; -fx-padding: 4px 6px 4px 6px;
-fx-cursor: hand; -fx-cursor: hand;
-color-button-border-hover: -color-button-border;
-color-button-border-focused: -color-button-border; -color-button-border-focused: -color-button-border;
-color-button-border-pressed: -color-button-border; -color-button-border-pressed: -color-button-border;
} }
@ -21,6 +25,27 @@ public class Tag extends Button {
public Tag(String text, Node graphic) { public Tag(String text, Node graphic) {
super(text, graphic); super(text, graphic);
if (graphic != null) {
graphic.setOnMouseEntered(e -> {
if (getContentDisplay() == RIGHT) {
graphic.setScaleX(1.2);
graphic.setScaleY(1.2);
}
});
graphic.setOnMouseExited(e -> {
if (getContentDisplay() == RIGHT) {
graphic.setScaleX(1);
graphic.setScaleY(1);
}
});
graphic.setOnMouseClicked(e -> {
if (getContentDisplay() == RIGHT && getParent() != null && getParent() instanceof Pane pane) {
pane.getChildren().remove(this);
}
});
}
getStyleClass().add("tag"); getStyleClass().add("tag");
} }
} }

@ -30,6 +30,7 @@ public class TagSample extends GridPane {
add(iconTagSample(), 1, 0); add(iconTagSample(), 1, 0);
add(outlinedTagSample(), 0, 1); add(outlinedTagSample(), 0, 1);
add(closeableTagSample(), 1, 1); add(closeableTagSample(), 1, 1);
add(customColorTagSample(), 0, 2);
} }
private SampleBlock filledTagSample() { private SampleBlock filledTagSample() {
@ -114,4 +115,42 @@ public class TagSample extends GridPane {
return new SampleBlock("Removable", content); return new SampleBlock("Removable", content);
} }
private SampleBlock customColorTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
new CSSFragment("""
.brand {
-color-button-fg: -color-fg-emphasis;
-color-button-bg-hover: -color-button-bg;
-color-button-bg-pressed: -color-button-bg;
}
.twitter {
-color-button-bg: rgb(85, 172, 238);
-color-button-border: rgb(85, 172, 238);
}
.youtube {
-color-button-bg: rgb(205, 32, 31);
-color-button-border: rgb(205, 32, 31);
}
.facebook {
-color-button-bg: rgb(59, 89, 153);
-color-button-border: rgb(59, 89, 153);
}
""").addTo(content);
var twitterTag = new Tag("Twitter", new FontIcon(Feather.TWITTER));
twitterTag.getStyleClass().addAll("brand", "twitter");
content.getChildren().add(twitterTag);
var youtubeTag = new Tag("YouTube", new FontIcon(Feather.YOUTUBE));
youtubeTag.getStyleClass().addAll("brand", "youtube");
content.getChildren().add(youtubeTag);
var facebookTag = new Tag("Facebook", new FontIcon(Feather.FACEBOOK));
facebookTag.getStyleClass().addAll("brand", "facebook");
content.getChildren().add(facebookTag);
return new SampleBlock("Custom Color", content);
}
} }

@ -53,14 +53,10 @@ $color-button-bg-selected: if(cfg.$darkMode, -color-base-6, -color-base-1) !defa
} }
} }
// Sadly JavaFX devs have a mania to declare everyting private and final. // JavaFX doesn't export (open) Modena resources package, so the below links won't work.
// The below code isn't working, while there are no runtime errors and URLs are correct // Because of AtlantaFX themes are also distributed as single CSS files, it can't be fixed here.
// it's still not loaded for unknown reason. // You can copy Modena resources and override those URLs in your app CSS to tackle the problem
// There are to many images here and HMTLEditor itself is obsolete type of control, // or better use TinyMCE or any other modern JS WYSIWYG editor instead of this legacy control.
// so I don't want to use data-url. It's either be fixed at OpenJFX or not.
// Anyone who treats this as a problem can easily fix it by using addtitional CSS file.
// Just copy CSS rules from below and images from the OpenJFX repo and then use relative
// URLs. #javafx-bug
$image-path: "/com/sun/javafx/scene/control/skin/modena" !default; $image-path: "/com/sun/javafx/scene/control/skin/modena" !default;
.color-picker.html-editor-foreground { .color-picker.html-editor-foreground {

@ -48,24 +48,55 @@ Text {
} }
.label.accent { .label.accent {
-fx-text-fill: -color-accent-fg; -fx-text-fill: -color-accent-fg;
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-accent-fg;
-fx-fill: -color-accent-fg;
}
} }
.text.success { .text.success {
-fx-fill: -color-success-fg; -fx-fill: -color-success-fg;
} }
.label.success { .label.success {
-fx-text-fill: -color-success-fg; -fx-text-fill: -color-success-fg;
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-success-fg;
-fx-fill: -color-success-fg;
}
} }
.text.warning { .text.warning {
-fx-fill: -color-warning-fg; -fx-fill: -color-warning-fg;
} }
.label.warning { .label.warning {
-fx-text-fill: -color-warning-fg; -fx-text-fill: -color-warning-fg;
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-warning-fg;
-fx-fill: -color-warning-fg;
}
} }
.text.danger { .text.danger {
-fx-fill: -color-danger-fg; -fx-fill: -color-danger-fg;
} }
.label.danger { .label.danger {
-fx-text-fill: -color-danger-fg; -fx-text-fill: -color-danger-fg;
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-danger-fg;
-fx-fill: -color-danger-fg;
}
}
.text-muted {
-fx-fill: -color-fg-muted;
}
.label.text-muted {
-fx-text-fill: -color-accent-fg;
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-accent-fg;
-fx-fill: -color-accent-fg;
}
} }
// font weight // font weight