diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java index 357a497..0eb1679 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java @@ -108,7 +108,7 @@ class MainLayer extends BorderPane { // startup, no prev page, no animation if (getScene() == null) { - subLayerPane.getChildren().add(nextPage.getView()); + subLayerPane.getChildren().setAll(nextPage.getView()); return; } @@ -118,7 +118,7 @@ class MainLayer extends BorderPane { prevPage.reset(); // animate switching between pages - subLayerPane.getChildren().add(nextPage.getView()); + subLayerPane.getChildren().setAll(nextPage.getView()); var transition = new FadeTransition(Duration.millis(PAGE_TRANSITION_DURATION), nextPage.getView()); transition.setFromValue(0.0); transition.setToValue(1.0); diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java index 8eb6167..211bbd5 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java @@ -9,6 +9,7 @@ import atlantafx.sampler.page.general.ThemePage; import atlantafx.sampler.page.general.TypographyPage; import atlantafx.sampler.page.showcase.filemanager.FileManagerPage; import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage; +import atlantafx.sampler.page.showcase.widget.WidgetCollectionPage; import atlantafx.sampler.util.Containers; import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; @@ -209,7 +210,8 @@ class Sidebar extends StackPane { navLink(TreeTablePage.NAME, TreeTablePage.class), caption("SHOWCASE"), navLink(FileManagerPage.NAME, FileManagerPage.class), - navLink(MusicPlayerPage.NAME, MusicPlayerPage.class) + navLink(MusicPlayerPage.NAME, MusicPlayerPage.class), + navLink(WidgetCollectionPage.NAME, WidgetCollectionPage.class) ); } diff --git a/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java b/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java index bfda4bf..e8aff46 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java +++ b/sampler/src/main/java/atlantafx/sampler/page/SampleBlock.java @@ -7,14 +7,20 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import net.datafaker.Faker; +import org.kordamp.ikonli.feather.Feather; import java.util.Objects; +import java.util.Random; public class SampleBlock extends VBox { public static final int BLOCK_HGAP = 20; public static final int BLOCK_VGAP = 10; + protected static final Faker FAKER = new Faker(); + protected static final Random RANDOM = new Random(); + protected final Label titleLabel; protected final Node content; // can be either Pane or Control protected TextFlow descriptionText; @@ -58,4 +64,8 @@ public class SampleBlock extends VBox { VBox.setVgrow(content, Priority.NEVER); } } + + protected static Feather randomIcon() { + return Feather.values()[RANDOM.nextInt(Feather.values().length)]; + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Card.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Card.java new file mode 100644 index 0000000..b1dab47 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Card.java @@ -0,0 +1,160 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.theme.Styles; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; + +public class Card extends VBox { + + public static final String CSS = """ + .card { + -fx-background-color: -color-bg-default; + -fx-effect: dropshadow(three-pass-box, -color-shadow-default, 8, 0.5, 0, 2); + } + .card > .subtitle { + -fx-text-fill: -color-fg-muted; + -fx-padding: 0px 15px 10px 15px; + } + .card > .title, + .card > .body, + .card > .footer { + -fx-padding: 10px 15px 10px 15px; + } + """; + + private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); + private final ObjectProperty image = new SimpleObjectProperty<>(); + private final ObjectProperty body = new SimpleObjectProperty<>(); + private final ObjectProperty footer = new SimpleObjectProperty<>(); + + public Card() { + super(); + createView(); + } + + private void createView() { + var footerSep = new Separator(); + footerSep.getStyleClass().add(Styles.SMALL); + footerSep.managedProperty().bind(Bindings.createObjectBinding( + () -> footer.get() != null && footer.get().isManaged(), footer + )); + + getChildren().setAll( + createPlaceholder(), // title + createPlaceholder(), // subtitle + createPlaceholder(), // image + createPlaceholder(), // body + footerSep, + createPlaceholder() // footer + ); + + image.addListener( + (obs, old, val) -> setChild(0, val, "image") + ); + title.addListener( + (obs, old, val) -> setChild(1, val != null ? new Label(val) : null, "title", Styles.TITLE_4) + ); + subtitle.addListener( + (obs, old, val) -> setChild(2, val != null ? new Label(val) : null, "subtitle") + ); + body.addListener( + (obs, old, val) -> setChild(3, val, "body") + ); + footer.addListener( + (obs, old, val) -> setChild(5, val, "footer") + ); + + getStyleClass().addAll("card", Styles.BORDERED); + } + + private void setChild(int index, Node node, String... styleClass) { + if (node != null) { + for (var s : styleClass) { + if (!node.getStyleClass().contains(s)) { + node.getStyleClass().add(s); + } + } + getChildren().set(index, node); + } else { + getChildren().set(index, createPlaceholder()); + } + } + + public String getTitle() { + return title.get(); + } + + public StringProperty titleProperty() { + return title; + } + + public void setTitle(String title) { + this.title.set(title); + } + + public String getSubtitle() { + return subtitle.get(); + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle.set(subtitle); + } + + public ImageView getImage() { + return image.get(); + } + + public ObjectProperty imageProperty() { + return image; + } + + public void setImage(ImageView image) { + this.image.set(image); + } + + public Parent getBody() { + return body.get(); + } + + public ObjectProperty bodyProperty() { + return body; + } + + public void setBody(Parent body) { + this.body.set(body); + } + + public Parent getFooter() { + return footer.get(); + } + + public ObjectProperty footerProperty() { + return footer; + } + + public void setFooter(Parent footer) { + this.footer.set(footer); + } + + private Parent createPlaceholder() { + var g = new Group(); + g.setManaged(false); + return g; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/CardSample.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/CardSample.java new file mode 100644 index 0000000..8358fe7 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/CardSample.java @@ -0,0 +1,139 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.theme.Styles; +import atlantafx.sampler.Resources; +import atlantafx.sampler.theme.CSSFragment; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelReader; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import net.datafaker.Faker; + +import static atlantafx.sampler.page.Page.PAGE_HGAP; +import static atlantafx.sampler.page.Page.PAGE_VGAP; + +public class CardSample extends HBox { + + private static final Faker FAKER = new Faker(); + private static final int CARD_WIDTH = 300; + + public CardSample() { + new CSSFragment(Card.CSS).addTo(this); + + setSpacing(PAGE_HGAP); + setAlignment(Pos.TOP_CENTER); + setMinWidth(CARD_WIDTH * 2 + PAGE_HGAP); + getChildren().setAll( + // column 0 + new VBox( + PAGE_VGAP, + textFooterCard(), + titleTextCard(), + quoteCard() + ), + // column 1 + new VBox( + PAGE_VGAP, + imageTextCard(), + titleImageCard() + ) + ); + } + + private Card textFooterCard() { + var card = new Card(); + card.setMinWidth(CARD_WIDTH); + card.setMaxWidth(CARD_WIDTH); + + var text = new Text(FAKER.chuckNorris().fact()); + card.setBody(new TextFlow(text)); + + var btn = new Button("More!"); + btn.getStyleClass().addAll(Styles.ACCENT, Styles.BUTTON_OUTLINED); + btn.setOnAction(e -> text.setText(FAKER.chuckNorris().fact())); + + card.setFooter(new HBox(btn)); + + return card; + } + + private Card imageTextCard() { + var card = new Card(); + card.setMinWidth(CARD_WIDTH); + card.setMaxWidth(CARD_WIDTH); + + var image = new ImageView(new Image(Resources.getResourceAsStream("images/20_min_adventure.jpg"))); + image.setFitWidth(300); + image.setPreserveRatio(true); + card.setImage(image); + + var text = new Text(FAKER.rickAndMorty().quote()); + card.setBody(new TextFlow(text)); + + return card; + } + + private Card titleTextCard() { + var card = new Card(); + card.setMinWidth(CARD_WIDTH); + card.setMaxWidth(CARD_WIDTH); + + card.setTitle("Title"); + card.setSubtitle("Subtitle"); + + var text = new Text(FAKER.lorem().paragraph()); + card.setBody(new TextFlow(text)); + + return card; + } + + private Card titleImageCard() { + var card = new Card(); + card.setMinWidth(CARD_WIDTH); + card.setMaxWidth(CARD_WIDTH); + + var image = new Image(Resources.getResourceAsStream("images/pattern.jpg")); + PixelReader pixelReader = image.getPixelReader(); + var cropImage = new WritableImage(pixelReader, 0, 0, 300, 100); + + card.setImage(new ImageView(cropImage)); + card.setTitle("Title"); + + var text = new Text(FAKER.lorem().paragraph()); + card.setBody(new TextFlow(text)); + + return card; + } + + private Card quoteCard() { + var card = new Card(); + card.setMinWidth(CARD_WIDTH); + card.setMaxWidth(CARD_WIDTH); + + var quoteText = new Text(FAKER.bojackHorseman().quotes()); + quoteText.getStyleClass().add(Styles.TITLE_3); + + var authorText = new Text("Bojack Horseman"); + + card.setBody(new VBox( + 10, + new TextFlow(quoteText), + authorText + )); + + card.setFooter(new TextFlow( + new Text("Share on "), + new Hyperlink("Twitter") + )); + + return card; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Message.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Message.java new file mode 100644 index 0000000..a3b4c43 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Message.java @@ -0,0 +1,145 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.theme.Styles; +import javafx.animation.FadeTransition; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.util.Duration; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2AL; + +import java.util.Objects; +import java.util.function.Consumer; + +public class Message extends StackPane { + + private static final int ANIMATION_DURATION = 500; + + public enum Type { + INFO, SUCCESS, WARNING, DANGER + } + + public static final String CSS = """ + .message { + -color-message-bg: -color-bg-default; + -color-message-fg: -color-fg-default; + -fx-background-color: -color-message-bg; + -fx-border-color: -color-message-fg; + -fx-border-width: 0 0 0 5px; + -fx-pref-width: 600px; + -fx-alignment: TOP_LEFT; + } + .message > .header { + -fx-font-weight: bold; + } + .message Text { + -fx-fill: -color-message-fg; + } + .message > .button { + -color-button-fg: -color-message-fg; + } + .message.info { + -color-message-bg: -color-accent-subtle; + -color-message-fg: -color-accent-fg; + } + .message.success { + -color-message-bg: -color-success-subtle; + -color-message-fg: -color-success-fg; + } + .message.warning { + -color-message-bg: -color-warning-subtle; + -color-message-fg: -color-warning-fg; + } + .message.danger { + -color-message-bg: -color-danger-subtle; + -color-message-fg: -color-danger-fg; + } + """; + + private final Type type; + private final String header; + private final String text; + private Consumer closeHandler; + + public Message(Type type, String header, String text) { + this.type = Objects.requireNonNull(type); + this.header = header; + this.text = Objects.requireNonNull(text); + createView(); + } + + private void createView() { + if (header != null) { + var headerText = new Text(header); + headerText.getStyleClass().addAll("header"); + StackPane.setMargin(headerText, new Insets(10, 10, 0, 15)); + getChildren().add(headerText); + } + + var messageText = new TextFlow(new Text(text)); + if (header != null) { + StackPane.setMargin(messageText, new Insets(40, 10, 10, 15)); + } else { + StackPane.setMargin(messageText, new Insets(10, 10, 10, 15)); + } + messageText.maxWidthProperty().bind(widthProperty().subtract(50)); + getChildren().add(messageText); + + var closeBtn = new Button("", new FontIcon(Material2AL.CLOSE)); + closeBtn.getStyleClass().addAll(Styles.BUTTON_CIRCLE, Styles.FLAT); + closeBtn.setOnAction(e -> handleClose()); + StackPane.setMargin(closeBtn, new Insets(2)); + StackPane.setAlignment(closeBtn, Pos.TOP_RIGHT); + getChildren().add(closeBtn); + + parentProperty().addListener((obs, old, val) -> { + if (val != null) { handleOpen(); } + }); + + getStyleClass().setAll("message", type.name().toLowerCase()); + } + + public Type getType() { + return type; + } + + public String getHeader() { + return header; + } + + public String getText() { + return text; + } + + public void setCloseHandler(Consumer closeHandler) { + this.closeHandler = closeHandler; + } + + private void handleOpen() { + var transition = new FadeTransition(new Duration(500), this); + transition.setFromValue(0); + transition.setToValue(1); + transition.play(); + } + + private void handleClose() { + var transition = new FadeTransition(new Duration(ANIMATION_DURATION), this); + transition.setFromValue(1); + transition.setToValue(0); + transition.setOnFinished(e -> { + if (getParent() != null && getParent() instanceof Pane pane) { + pane.getChildren().remove(this); + } + if (closeHandler != null) { + closeHandler.accept(this); + } + }); + transition.play(); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/MessageSample.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/MessageSample.java new file mode 100644 index 0000000..fefc05b --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/MessageSample.java @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.sampler.page.SampleBlock; +import atlantafx.sampler.theme.CSSFragment; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import java.util.function.Consumer; + +public class MessageSample extends SampleBlock { + + public MessageSample() { + super("Message", createContent()); + } + + private static Node createContent() { + var content = new VBox(BLOCK_VGAP); + content.setAlignment(Pos.TOP_CENTER); + VBox.setVgrow(content, Priority.ALWAYS); + new CSSFragment(Message.CSS).addTo(content); + + var closeHandler = new Consumer() { + @Override + public void accept(Message msg) { + var newMsg = new Message(msg.getType(), msg.getHeader(), FAKER.chuckNorris().fact()); + newMsg.setCloseHandler(this); + content.getChildren().add(newMsg); + } + }; + + for (Message.Type type : Message.Type.values()) { + var msg = new Message(type, type.name(), FAKER.chuckNorris().fact()); + msg.setCloseHandler(closeHandler); + content.getChildren().add(msg); + } + + return content; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Stepper.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Stepper.java new file mode 100644 index 0000000..6c6f73f --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Stepper.java @@ -0,0 +1,241 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.css.PseudoClass; +import javafx.geometry.Pos; +import javafx.geometry.Side; +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.kordamp.ikonli.Ikon; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class Stepper extends HBox { + + public static final String CSS = """ + .stepper { + -color-stepper-bg: -color-bg-subtle; + -color-stepper-fg: -color-fg-default; + -color-stepper-border: -color-border-default; + -fx-pref-width: 600px; + -fx-spacing: 10px; + } + .stepper > .item { + -fx-graphic-text-gap: 10px; + } + .stepper > .item > .graphic { + -fx-font-size: 1.1em; + -fx-min-width: 2.2em; + -fx-max-width: 2.2em; + -fx-min-height: 2.2em; + -fx-max-height: 2.2em; + -fx-text-fill: -color-stepper-fg; + -fx-background-color: -color-stepper-bg; + -fx-background-radius: 10em; + -fx-border-color: -color-stepper-border; + -fx-border-radius: 10em; + -fx-border-width: 1; + -fx-alignment: CENTER; + } + .stepper > .item .ikonli-font-icon { + -fx-fill: -color-stepper-fg; + -fx-icon-color: -color-stepper-fg; + } + .stepper > .item:selected > .graphic { + -color-stepper-bg: -color-accent-subtle; + -color-stepper-fg: -color-accent-fg; + -color-stepper-border: -color-accent-emphasis; + } + .stepper > .item:completed { + -color-stepper-bg: -color-accent-emphasis; + -color-stepper-fg: -color-fg-emphasis; + -color-stepper-border: -color-accent-emphasis; + } + """; + + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + private static final PseudoClass COMPLETED = PseudoClass.getPseudoClass("completed"); + + private final List items; + private final ObjectProperty textPosition = new SimpleObjectProperty<>(Side.LEFT); + private final ObjectProperty selectedItem = new SimpleObjectProperty<>(); + private final BooleanBinding canGoBack; + private final BooleanBinding canGoForward; + + public Stepper(Item... items) { + this(Arrays.asList(items)); + } + + public Stepper(List items) { + if (items == null || items.isEmpty()) { + throw new IllegalArgumentException("Item list can't be null or empty."); + } + + this.items = Collections.unmodifiableList(items); + + canGoBack = Bindings.createBooleanBinding(() -> { + if (selectedItem.get() == null) { return false; } + var idx = items.indexOf(selectedItem.get()); + return idx > 0 && idx <= items.size() - 1; + }, selectedItem); + + canGoForward = Bindings.createBooleanBinding(() -> { + if (selectedItem.get() == null) { return false; } + var idx = items.indexOf(selectedItem.get()); + return idx >= 0 && idx < items.size() - 1; + }, selectedItem); + + selectedItem.addListener((obs, old, val) -> { + if (old != null) { old.pseudoClassStateChanged(SELECTED, false); } + if (val != null) { val.pseudoClassStateChanged(SELECTED, true); } + }); + + createView(); + } + + private void createView() { + alignmentProperty().bind(Bindings.createObjectBinding(() -> + switch (textPositionProperty().get()) { + case TOP -> Pos.TOP_LEFT; + case BOTTOM -> Pos.BOTTOM_LEFT; + default -> Pos.CENTER_LEFT; + }, textPositionProperty()) + ); + + updateItems(); + getStyleClass().add("stepper"); + } + + private void updateItems() { + var children = new ArrayList(); + for (int i = 0; i < items.size(); i++) { + var item = items.get(i); + item.contentDisplayProperty().bind(Bindings.createObjectBinding(() -> + switch (textPositionProperty().get()) { + case TOP -> ContentDisplay.TOP; + case BOTTOM -> ContentDisplay.BOTTOM; + case LEFT -> ContentDisplay.LEFT; + case RIGHT -> ContentDisplay.RIGHT; + }, textPositionProperty()) + ); + + children.add(item); + + if (i < items.size() - 1) { + var sep = new Separator(); + HBox.setHgrow(sep, Priority.ALWAYS); + children.add(sep); + } + } + getChildren().setAll(children); + } + + public List getItems() { + return items; + } + + public Side getTextPosition() { + return textPosition.get(); + } + + public void setTextPosition(Side textPosition) { + this.textPosition.set(textPosition); + } + + public ObjectProperty textPositionProperty() { + return textPosition; + } + + public Item getSelectedItem() { + return selectedItem.get(); + } + + public ObjectProperty selectedItemProperty() { + return selectedItem; + } + + public void setSelectedItem(Item selectedItem) { + this.selectedItem.set(selectedItem); + } + + public BooleanBinding canGoBackProperty() { + return canGoBack; + } + + public void backward() { + if (!canGoBack.get()) { return; } + var idx = items.indexOf(selectedItem.get()); + selectedItem.set(items.get(idx - 1)); + } + + public BooleanBinding canGoForwardProperty() { + return canGoForward; + } + + public void forward() { + if (!canGoForward.get()) { return; } + var idx = items.indexOf(selectedItem.get()); + selectedItem.set(items.get(idx + 1)); + } + + /////////////////////////////////////////////////////////////////////////// + + public static class Item extends Label { + + private final BooleanProperty completed = new SimpleBooleanProperty(); + + public Item(String text) { + super(text); + + var graphicLabel = new Label(); + graphicLabel.getStyleClass().add("graphic"); + setGraphic(graphicLabel); + + completed.addListener((obs, old, val) -> pseudoClassStateChanged(COMPLETED, val)); + getStyleClass().add("item"); + setContentDisplay(ContentDisplay.LEFT); + } + + public void setGraphic(Ikon icon) { + var graphicLabel = ((Label) getGraphic()); + if (icon != null) { + graphicLabel.setText(null); + graphicLabel.setGraphic(new FontIcon(icon)); + } + } + + public void setGraphic(String text) { + var graphicLabel = ((Label) getGraphic()); + if (text != null) { + graphicLabel.setText(text); + graphicLabel.setGraphic(null); + } + } + + public boolean isCompleted() { + return completed.get(); + } + + public void setCompleted(boolean state) { + completed.set(state); + } + + public BooleanProperty completedProperty() { + return completed; + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/StepperSample.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/StepperSample.java new file mode 100644 index 0000000..6df1a8d --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/StepperSample.java @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.controls.Spacer; +import atlantafx.base.controls.ToggleSwitch; +import atlantafx.base.theme.Styles; +import atlantafx.sampler.page.SampleBlock; +import atlantafx.sampler.page.showcase.widget.Stepper.Item; +import atlantafx.sampler.theme.CSSFragment; +import javafx.geometry.Pos; +import javafx.geometry.Side; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2MZ; + +public class StepperSample extends SampleBlock { + + public StepperSample() { + super("Stepper", createContent()); + } + + private static Node createContent() { + var content = new VBox(BLOCK_VGAP); + new CSSFragment(Stepper.CSS).addTo(content); + + // == STEPPER CONTENT == + + var stackContent = new Label(); + stackContent.getStyleClass().add(Styles.TITLE_1); + stackContent.setStyle("-fx-background-color:-color-bg-default;"); + stackContent.setWrapText(true); + stackContent.setMinHeight(200); + stackContent.setMaxWidth(400); + stackContent.setAlignment(Pos.CENTER); + + var stack = new StackPane(stackContent); + stack.setPrefHeight(200); + + // == STEPPER == + + var firstItem = new Item("First"); + firstItem.setGraphic("A"); + + var secondItem = new Item("Second"); + secondItem.setGraphic("B"); + + var thirdItem = new Item("Third"); + thirdItem.setGraphic("C"); + + var stepper = new Stepper(firstItem, secondItem, thirdItem); + stepper.selectedItemProperty().addListener( + (obs, old, val) -> stackContent.setText(val != null ? val.getText() : null) + ); + stepper.setSelectedItem(stepper.getItems().get(0)); + + // == CONTROLS == + + var saveBtn = new Button("Save"); + saveBtn.setDefaultButton(true); + saveBtn.setOnAction(e -> { + // you can validate user input before moving forward here + stepper.getSelectedItem().setCompleted(true); + stepper.forward(); + }); + + var cancelBtn = new Button("Cancel"); + cancelBtn.getStyleClass().addAll(Styles.FLAT); + cancelBtn.setOnAction(e -> { + stepper.getSelectedItem().setCompleted(false); + stepper.backward(); + }); + + var iconToggle = new ToggleSwitch("Icons"); + iconToggle.selectedProperty().addListener((obs, old, val) -> { + for (int i = 0; i < stepper.getItems().size(); i++) { + var item = stepper.getItems().get(i); + if (val) { + item.setGraphic(randomIcon()); + } else { + item.setGraphic(String.valueOf(i + 1)); + } + } + }); + + var rotateBtn = new Button("Rotate", new FontIcon(Material2MZ.ROTATE_RIGHT)); + rotateBtn.setOnAction(e -> { + Side nextSide = switch (stepper.getTextPosition()) { + case LEFT -> Side.TOP; + case TOP -> Side.RIGHT; + case RIGHT -> Side.BOTTOM; + case BOTTOM -> Side.LEFT; + }; + stepper.setTextPosition(nextSide); + }); + + var controls = new HBox( + BLOCK_HGAP, + saveBtn, + cancelBtn, + new Spacer(), + iconToggle, + rotateBtn + ); + controls.setAlignment(Pos.CENTER_LEFT); + + // ~ + + content.getChildren().setAll(stepper, stack, new Separator(), controls); + return content; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Tag.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Tag.java new file mode 100644 index 0000000..7ffb460 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/Tag.java @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import javafx.scene.Node; +import javafx.scene.control.Button; + +public class Tag extends Button { + + public static final String CSS = """ + .tag { + -fx-padding: 4px 6px 4px 6px; + -fx-cursor: hand; + -color-button-border-focused: -color-button-border; + -color-button-border-pressed: -color-button-border; + } + """; + + public Tag(String text) { + this(text, null); + } + + public Tag(String text, Node graphic) { + super(text, graphic); + getStyleClass().add("tag"); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/TagSample.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/TagSample.java new file mode 100644 index 0000000..cde403e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/TagSample.java @@ -0,0 +1,117 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.theme.Styles; +import atlantafx.sampler.page.SampleBlock; +import atlantafx.sampler.theme.CSSFragment; +import javafx.scene.control.ContentDisplay; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import org.kordamp.ikonli.feather.Feather; +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_VGAP; +import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP; +import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; + +public class TagSample extends GridPane { + + private static final int PREF_WIDTH = 300; + + public TagSample() { + new CSSFragment(Tag.CSS).addTo(this); + + setHgap(PAGE_HGAP); + setVgap(PAGE_VGAP); + + add(filledTagSample(), 0, 0); + add(iconTagSample(), 1, 0); + add(outlinedTagSample(), 0, 1); + add(closeableTagSample(), 1, 1); + } + + private SampleBlock filledTagSample() { + var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); + content.setPrefWidth(PREF_WIDTH); + + var basicTag = new Tag("basic"); + content.getChildren().add(basicTag); + + var accentTag = new Tag("accent"); + accentTag.getStyleClass().add(Styles.ACCENT); + content.getChildren().add(accentTag); + + var successTag = new Tag("success"); + successTag.getStyleClass().add(Styles.SUCCESS); + content.getChildren().add(successTag); + + var dangerTag = new Tag("danger"); + dangerTag.getStyleClass().add(Styles.DANGER); + content.getChildren().add(dangerTag); + + return new SampleBlock("Filled", content); + } + + private SampleBlock iconTagSample() { + var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); + content.setPrefWidth(PREF_WIDTH); + + var basicTag = new Tag("image", new FontIcon(Feather.IMAGE)); + content.getChildren().add(basicTag); + + var accentTag = new Tag("music", new FontIcon(Feather.MUSIC)); + content.getChildren().add(accentTag); + + var successTag = new Tag("video", new FontIcon(Feather.VIDEO)); + content.getChildren().add(successTag); + + return new SampleBlock("Icon", content); + } + + private SampleBlock outlinedTagSample() { + var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); + content.setPrefWidth(PREF_WIDTH); + + var accentTag = new Tag("accent"); + accentTag.getStyleClass().addAll(Styles.ACCENT, Styles.BUTTON_OUTLINED); + content.getChildren().add(accentTag); + + var successTag = new Tag("success"); + successTag.getStyleClass().addAll(Styles.SUCCESS, Styles.BUTTON_OUTLINED); + content.getChildren().add(successTag); + + var dangerTag = new Tag("danger"); + dangerTag.getStyleClass().addAll(Styles.DANGER, Styles.BUTTON_OUTLINED); + content.getChildren().add(dangerTag); + + return new SampleBlock("Outlined", content); + } + + private SampleBlock closeableTagSample() { + var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); + content.setPrefWidth(PREF_WIDTH); + + var basicTag = new Tag("basic", new FontIcon(Material2AL.CLOSE)); + basicTag.setContentDisplay(ContentDisplay.RIGHT); + content.getChildren().add(basicTag); + + var accentTag = new Tag("accent", new FontIcon(Material2AL.CANCEL)); + accentTag.setContentDisplay(ContentDisplay.RIGHT); + accentTag.getStyleClass().add(Styles.ACCENT); + content.getChildren().add(accentTag); + + var successTag = new Tag("success", new FontIcon(Material2AL.CANCEL)); + successTag.setContentDisplay(ContentDisplay.RIGHT); + successTag.getStyleClass().add(Styles.SUCCESS); + content.getChildren().add(successTag); + + var dangerTag = new Tag("danger", new FontIcon(Material2AL.CANCEL)); + dangerTag.setContentDisplay(ContentDisplay.RIGHT); + dangerTag.getStyleClass().add(Styles.DANGER); + content.getChildren().add(dangerTag); + + return new SampleBlock("Removable", content); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/WidgetCollectionPage.java b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/WidgetCollectionPage.java new file mode 100644 index 0000000..704327d --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/showcase/widget/WidgetCollectionPage.java @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.showcase.widget; + +import atlantafx.base.theme.Styles; +import atlantafx.base.theme.Tweaks; +import atlantafx.sampler.page.Page; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Parent; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; + +import java.util.function.Supplier; + +// JavaFX Skin API is very complex and almost undocumented. In many cases it's much simpler +// to create a small widget that just do the job than wasting hours to debug control behaviour. +// Consider this as a cookbook of those widgets. +public class WidgetCollectionPage extends BorderPane implements Page { + + public static final String NAME = "Widgets"; + + @Override + public String getName() { return NAME; } + + private final ListView toc = new ListView<>(); + private final VBox widgetWrapper = new VBox(PAGE_HGAP); + private boolean isRendered = false; + + public WidgetCollectionPage() { + super(); + createView(); + } + + private void createView() { + widgetWrapper.getStyleClass().add("widget"); + widgetWrapper.setAlignment(Pos.TOP_CENTER); + widgetWrapper.setFillWidth(false); + + toc.setCellFactory(c -> new TocCell()); + toc.getStyleClass().addAll("toc", Styles.DENSE, Tweaks.EDGE_TO_EDGE); + toc.getItems().setAll(Example.values()); + toc.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> { + if (val == null) { return; } + widgetWrapper.getChildren().setAll(val.getSupplier().get()); + }); + + // ~ + + setCenter(widgetWrapper); + setRight(toc); + BorderPane.setMargin(toc, new Insets(0, 0, 0, PAGE_HGAP)); + getStyleClass().setAll("page", "widget-collection"); + + toc.getSelectionModel().selectFirst(); + } + + @Override + public Parent getView() { + return this; + } + + @Override + public boolean canDisplaySourceCode() { + return false; + } + + @Override + public boolean canChangeThemeSettings() { + return true; + } + + @Override + public void reset() { } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (isRendered) { return; } + + isRendered = true; + toc.getSelectionModel().selectFirst(); + toc.requestFocus(); + } + + /////////////////////////////////////////////////////////////////////////// + + public enum Example { + CARD("Card", () -> new CardSample()), + MESSAGE("Message", () -> new MessageSample()), + STEPPER("Stepper", () -> new StepperSample()), + TAG("Tag", () -> new TagSample()); + + private final String name; + private final Supplier supplier; + + Example(String name, Supplier supplier) { + this.name = name; + this.supplier = supplier; + } + + public String getName() { + return name; + } + + public Supplier getSupplier() { + return supplier; + } + } + + private static class TocCell extends ListCell { + + @Override + protected void updateItem(Example item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getName()); + } + } + } +} diff --git a/sampler/src/main/resources/assets/styles/scss/layout/_page.scss b/sampler/src/main/resources/assets/styles/scss/layout/_page.scss index e92a9b2..b0f012e 100644 --- a/sampler/src/main/resources/assets/styles/scss/layout/_page.scss +++ b/sampler/src/main/resources/assets/styles/scss/layout/_page.scss @@ -16,6 +16,18 @@ } } } + + &.widget-collection { + -fx-padding: 40px; + -fx-min-width: 800px; + -fx-max-width: 1200px; + + >.toc { + -fx-pref-width: 150px; + -color-cell-bg-selected: -color-cell-bg; + -color-cell-fg-selected: -color-accent-fg; + } + } } .sample-block { @@ -31,4 +43,4 @@ -fx-font-weight: bold; -fx-padding: 0 0 10px 0; } -} \ No newline at end of file +} diff --git a/sampler/src/main/resources/images/pattern.jpg b/sampler/src/main/resources/images/pattern.jpg new file mode 100644 index 0000000..2640586 Binary files /dev/null and b/sampler/src/main/resources/images/pattern.jpg differ