Add Card control

This commit is contained in:
mkpaz 2023-05-24 16:39:16 +04:00
parent 47e2d4b9a5
commit 128836a550
9 changed files with 422 additions and 4 deletions

@ -0,0 +1,101 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
/**
* Aa versatile container that can used in various contexts such as headings,
* text, dialogs and more. It includes a header to provide a brief overview
* or context of the information. The sub-header and body sections provide
* more detailed content, while the footer may include additional actions or
* information.
*/
public class Card extends Control {
// Default constructor
public Card() {
super();
}
@Override
protected Skin<?> createDefaultSkin() {
return new CardSkin(this);
}
///////////////////////////////////////////////////////////////////////////
// Properties //
///////////////////////////////////////////////////////////////////////////
/**
* The property representing the cards header node.
*/
private final ObjectProperty<Node> header = new SimpleObjectProperty<>(this, "header");
public Node getHeader() {
return header.get();
}
public ObjectProperty<Node> headerProperty() {
return header;
}
public void setHeader(Node header) {
this.header.set(header);
}
/**
* The property representing the cards sub-header node.
*/
private final ObjectProperty<Node> subHeader = new SimpleObjectProperty<>(this, "subHeader");
public Node getSubHeader() {
return subHeader.get();
}
public final ObjectProperty<Node> subHeaderProperty() {
return subHeader;
}
public void setSubHeader(Node subHeader) {
this.subHeader.set(subHeader);
}
/**
* The property representing the cards body node.
*/
private final ObjectProperty<Node> body = new SimpleObjectProperty<>(this, "body");
public Node getBody() {
return body.get();
}
public ObjectProperty<Node> bodyProperty() {
return body;
}
public void setBody(Node body) {
this.body.set(body);
}
/**
* The property representing the cards footer node.
*/
private final ObjectProperty<Node> footer = new SimpleObjectProperty<>(this, "footer");
public Node getFooter() {
return footer.get();
}
public ObjectProperty<Node> footerProperty() {
return footer;
}
public void setFooter(Node footer) {
this.footer.set(footer);
}
}

@ -0,0 +1,78 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.beans.value.ChangeListener;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class CardSkin implements Skin<Card> {
protected final Card control;
protected final VBox root = new VBox();
protected final StackPane headerSlot;
protected final ChangeListener<Node> headerSlotListener;
protected final StackPane subHeaderSlot;
protected final ChangeListener<Node> subHeaderSlotListener;
protected final StackPane bodySlot;
protected final ChangeListener<Node> bodySlotListener;
protected final StackPane footerSlot;
protected final ChangeListener<Node> footerSlotListener;
protected CardSkin(Card control) {
this.control = control;
headerSlot = new StackPane();
headerSlot.getStyleClass().add("header");
headerSlotListener = new SlotListener(headerSlot);
control.headerProperty().addListener(headerSlotListener);
headerSlotListener.changed(control.headerProperty(), null, control.getHeader());
subHeaderSlot = new StackPane();
subHeaderSlot.getStyleClass().add("sub-header");
subHeaderSlotListener = new SlotListener(subHeaderSlot);
control.subHeaderProperty().addListener(subHeaderSlotListener);
subHeaderSlotListener.changed(control.subHeaderProperty(), null, control.getSubHeader());
bodySlot = new StackPane();
bodySlot.getStyleClass().add("body");
VBox.setVgrow(bodySlot, Priority.ALWAYS);
bodySlotListener = new SlotListener(bodySlot);
control.bodyProperty().addListener(bodySlotListener);
bodySlotListener.changed(control.bodyProperty(), null, control.getBody());
footerSlot = new StackPane();
footerSlot.getStyleClass().add("footer");
footerSlotListener = new SlotListener(footerSlot);
control.footerProperty().addListener(footerSlotListener);
footerSlotListener.changed(control.footerProperty(), null, control.getFooter());
root.getStyleClass().add("card");
root.getChildren().setAll(headerSlot, subHeaderSlot, bodySlot, footerSlot);
}
@Override
public Card getSkinnable() {
return control;
}
@Override
public Node getNode() {
return root;
}
@Override
public void dispose() {
control.headerProperty().removeListener(headerSlotListener);
control.subHeaderProperty().removeListener(subHeaderSlotListener);
control.bodyProperty().removeListener(bodySlotListener);
control.footerProperty().removeListener(footerSlotListener);
}
}

@ -53,6 +53,7 @@ public class TileSkin extends SkinBase<Tile> {
headerBox.setMinHeight(Region.USE_COMPUTED_SIZE);
headerBox.setPrefHeight(Region.USE_COMPUTED_SIZE);
headerBox.setMaxHeight(Region.USE_COMPUTED_SIZE);
headerBox.pseudoClassStateChanged(FILLED, control.getSubTitle() != null);
registerChangeListener(control.subTitleProperty(), o -> {
var value = getSkinnable().getSubTitle();

@ -11,6 +11,7 @@ import atlantafx.sampler.page.components.AnimationsPage;
import atlantafx.sampler.page.components.BreadcrumbsPage;
import atlantafx.sampler.page.components.ButtonPage;
import atlantafx.sampler.page.components.CalendarPage;
import atlantafx.sampler.page.components.CardPage;
import atlantafx.sampler.page.components.ChartPage;
import atlantafx.sampler.page.components.CheckBoxPage;
import atlantafx.sampler.page.components.ChoiceBoxPage;
@ -158,6 +159,7 @@ public class MainModel {
var containers = NavTree.Item.group("Containers", new FontIcon(Material2OutlinedMZ.TABLE_CHART));
containers.getChildren().setAll(
NAV_TREE.get(AccordionPage.class),
NAV_TREE.get(CardPage.class),
NAV_TREE.get(ContextMenuPage.class),
NAV_TREE.get(DeckPanePage.class),
NAV_TREE.get(ModalPanePage.class),
@ -260,6 +262,7 @@ public class MainModel {
map.put(BreadcrumbsPage.class, NavTree.Item.page(BreadcrumbsPage.NAME, BreadcrumbsPage.class));
map.put(ButtonPage.class, NavTree.Item.page(ButtonPage.NAME, ButtonPage.class));
map.put(CalendarPage.class, NavTree.Item.page(CalendarPage.NAME, CalendarPage.class));
map.put(CardPage.class, NavTree.Item.page(CardPage.NAME, CardPage.class));
map.put(ChartPage.class, NavTree.Item.page(ChartPage.NAME, ChartPage.class));
map.put(ChoiceBoxPage.class, NavTree.Item.page(ChoiceBoxPage.NAME, ChoiceBoxPage.class));
map.put(CheckBoxPage.class, NavTree.Item.page(CheckBoxPage.NAME, CheckBoxPage.class));

@ -0,0 +1,176 @@
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Card;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.net.URI;
import java.util.function.Function;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ;
public class CardPage extends OutlinePage {
public static final String NAME = "Card";
@Override
public String getName() {
return NAME;
}
@Override
public URI getJavadocUri() {
return URI.create(String.format(AFX_JAVADOC_URI_TEMPLATE, "controls/" + getName()));
}
public CardPage() {
super();
addPageHeader();
addFormattedText("""
The [i]Card[/i] is a versatile container that can used in various contexts \
such as headings, text, dialogs and more. It includes a header to provide a \
brief overview or context of the information. The sub-header and body sections \
provide more detailed content, while the footer may include additional actions \
or information."""
);
addNode(skeleton());
addSection("Usage", usageExample());
addSection("Elevation", elevationExample());
}
private Node skeleton() {
Function<String, VBox> cellBuilder = s -> {
var lbl = new Label(s);
lbl.getStyleClass().add(Styles.TEXT_SMALL);
var cell = new VBox(lbl);
cell.setPadding(new Insets(10));
cell.setFillWidth(true);
cell.setAlignment(Pos.CENTER);
Styles.appendStyle(cell, "-fx-border-color", "-color-accent-muted");
Styles.appendStyle(cell, "-fx-border-width", "5px");
return cell;
};
var body = cellBuilder.apply("body");
body.setPadding(new Insets(20, 10, 20, 10));
var box = new VBox(10);
box.setMaxWidth(500);
box.getChildren().setAll(
cellBuilder.apply("header"),
cellBuilder.apply("sub-header"),
body,
cellBuilder.apply("footer")
);
return box;
}
private ExampleBox usageExample() {
//snippet_1:start
var tweetCard = new Card();
tweetCard.setMinWidth(300);
tweetCard.setMaxWidth(300);
var title = new Label(FAKER.twitter().userName());
title.getStyleClass().add(Styles.TITLE_4);
tweetCard.setHeader(title);
var text = new TextFlow(new Text(
FAKER.lorem().sentence(20)
));
text.setMaxWidth(260);
tweetCard.setBody(text);
var footer = new HBox(10,
new FontIcon(Material2AL.FAVORITE),
new Label("861"),
new Spacer(20),
new FontIcon(Material2MZ.SHARE),
new Label("92")
);
footer.setAlignment(Pos.CENTER_LEFT);
tweetCard.setFooter(footer);
// ~
var dialogCard = new Card();
dialogCard.setHeader(new Tile(
"Delete content",
"Are you sure to remove this content? "
+ "You can access this file for 7 days in your trash."
));
dialogCard.setBody(new CheckBox("Do not show it anymore"));
var confirmBtn = new Button("Confirm");
confirmBtn.setDefaultButton(true);
confirmBtn.setPrefWidth(150);
var cancelBtn = new Button("Cancel");
cancelBtn.setPrefWidth(150);
var dialogFooter = new HBox(20, confirmBtn, cancelBtn);
dialogCard.setFooter(dialogFooter);
//snippet_1:end
var box = new HBox(HGAP_20, tweetCard, dialogCard);
var description = BBCodeParser.createFormattedText("""
The [i]Card[/i] pairs well with the [i]Tile[/i] component. \
You can use the [i]Tile[/i] as either a header or body for the [i]Card[/i]. \
Its also suitable for building more complex dialogs as well."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox elevationExample() {
//snippet_2:start
var card1 = new Card();
card1.getStyleClass().add(Styles.ELEVATED_2);
card1.setMinWidth(250);
card1.setMaxWidth(250);
card1.setHeader(new Tile(
"This is a title",
"This is a subtitle"
));
card1.setBody(new Label("This is content"));
var card2 = new Card();
card2.getStyleClass().add(Styles.INTERACTIVE);
card2.setMinWidth(250);
card2.setMaxWidth(250);
card2.setHeader(new Tile(
"This is a title",
"This is a subtitle"
));
card2.setBody(new Label("This is content"));
//snippet_2:end
var box = new HBox(HGAP_20, card1, card2);
box.setPadding(new Insets(0, 0, 10, 0));
var description = BBCodeParser.createFormattedText("""
With [code]Styles.ELEVATED_N[/code] or [code]Styles.INTERACTIVE[/code] styles classes \
you can add raised shadow effect to the [i]Card[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
}

@ -61,7 +61,10 @@ public class TilePage extends OutlinePage {
private Node skeleton() {
BiFunction<String, Pos, Node> cellBuilder = (s, pos) -> {
var cell = new VBox(new Label(s));
var lbl = new Label(s);
lbl.getStyleClass().add(Styles.TEXT_SMALL);
var cell = new VBox(lbl);
cell.setPadding(new Insets(10));
cell.setFillWidth(true);
cell.setAlignment(pos);

@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
@use "../settings/config" as cfg;
@use "../settings/effects";
$color-bg: -color-bg-default !default;
$color-border: -color-border-default !default;
$padding-x: 0.75em !default;
$padding-y: 1em !default;
$spacing: 10px !default;
.card {
-fx-background-color: $color-bg;
-fx-alignment: TOP_LEFT;
-fx-padding: $padding-y $padding-x $padding-y $padding-x;
-fx-spacing: $spacing;
-fx-border-color: $color-border;
-fx-border-width: cfg.$border-width;
-fx-border-radius: cfg.$border-radius;
>.header {
-fx-alignment: TOP_LEFT;
}
>.sub-header {
-fx-alignment: TOP_LEFT;
}
>.body {
// double spacing for body
-fx-padding: $spacing 0 $spacing 0;
-fx-alignment: TOP_LEFT;
}
>.footer {
-fx-alignment: TOP_LEFT;
}
@each $level, $radius in cfg.$elevation {
&.elevated-#{$level} {
@include effects.shadow(cfg.$elevation-color, $radius);
}
}
&.interactive:hover {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive);
}
.tile {
// prevent double indentation
-fx-padding: 0;
-fx-background-radius: 0;
}
}

@ -3,6 +3,7 @@
@use "accordion";
@use "breadcrumbs";
@use "button";
@use "card";
@use "chart";
@use "checkbox";
@use "color-picker";

@ -1,9 +1,9 @@
// SPDX-License-Identifier: MIT
$color-interactive: -color-bg-subtle !default;
@use "../settings/config" as cfg;
$color-interactive: -color-bg-subtle !default;
.tile {
-fx-padding: 0.75em 1em 0.75em 1em;
-fx-alignment: CENTER_LEFT;
@ -33,7 +33,7 @@ $color-interactive: -color-bg-subtle !default;
}
&:filled {
-fx-spacing: 5px;
-fx-spacing: 0.5em;
-fx-alignment: TOP_LEFT;
}
}