Add Card control
This commit is contained in:
parent
47e2d4b9a5
commit
128836a550
101
base/src/main/java/atlantafx/base/controls/Card.java
Normal file
101
base/src/main/java/atlantafx/base/controls/Card.java
Normal file
@ -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 card’s 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 card’s 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 card’s 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 card’s 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);
|
||||
}
|
||||
}
|
78
base/src/main/java/atlantafx/base/controls/CardSkin.java
Normal file
78
base/src/main/java/atlantafx/base/controls/CardSkin.java
Normal file
@ -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]. \
|
||||
It’s 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);
|
||||
|
55
styles/src/components/_card.scss
Normal file
55
styles/src/components/_card.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user