Refactor and improve Card

This commit is contained in:
mkpaz 2023-05-29 16:40:08 +04:00
parent 34acefa8f8
commit 24a2e096ad
7 changed files with 184 additions and 51 deletions

@ -20,6 +20,7 @@ public class Card extends Control {
// Default constructor
public Card() {
super();
getStyleClass().add("card");
}
@Override

@ -3,14 +3,22 @@
package atlantafx.base.controls;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
public class CardSkin implements Skin<Card> {
protected static final PseudoClass HAS_HEADER = PseudoClass.getPseudoClass("has-header");
protected static final PseudoClass HAS_SUBHEADER = PseudoClass.getPseudoClass("has-subheader");
protected static final PseudoClass HAS_BODY = PseudoClass.getPseudoClass("has-body");
protected static final PseudoClass HAS_FOOTER = PseudoClass.getPseudoClass("has-footer");
protected static final PseudoClass HAS_IMAGE = PseudoClass.getPseudoClass("has-image");
protected final Card control;
protected final VBox root = new VBox();
@ -31,30 +39,42 @@ public class CardSkin implements Skin<Card> {
headerSlot = new StackPane();
headerSlot.getStyleClass().add("header");
headerSlotListener = new SlotListener(headerSlot);
headerSlotListener = new SlotListener(
headerSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_HEADER, active)
);
control.headerProperty().addListener(headerSlotListener);
headerSlotListener.changed(control.headerProperty(), null, control.getHeader());
subHeaderSlot = new StackPane();
subHeaderSlot.getStyleClass().add("sub-header");
subHeaderSlotListener = new SlotListener(subHeaderSlot);
subHeaderSlotListener = new SlotListener(
subHeaderSlot,
(n, active) -> {
getSkinnable().pseudoClassStateChanged(HAS_SUBHEADER, active);
getSkinnable().pseudoClassStateChanged(HAS_IMAGE, n instanceof ImageView);
}
);
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);
bodySlotListener = new SlotListener(
bodySlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_BODY, active)
);
control.bodyProperty().addListener(bodySlotListener);
bodySlotListener.changed(control.bodyProperty(), null, control.getBody());
footerSlot = new StackPane();
footerSlot.getStyleClass().add("footer");
footerSlotListener = new SlotListener(footerSlot);
footerSlotListener = new SlotListener(
footerSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_FOOTER, active)
);
control.footerProperty().addListener(footerSlotListener);
footerSlotListener.changed(control.footerProperty(), null, control.getFooter());
root.getStyleClass().add("card");
root.getStyleClass().add("container");
root.getChildren().setAll(headerSlot, subHeaderSlot, bodySlot, footerSlot);
}

@ -11,7 +11,7 @@ import javafx.scene.layout.StackPane;
public class MessageSkin extends TileSkinBase<Message> {
private static final PseudoClass CLOSEABLE = PseudoClass.getPseudoClass("closeable");
protected static final PseudoClass CLOSEABLE = PseudoClass.getPseudoClass("closeable");
protected final StackPane closeButton = new StackPane();
protected final StackPane closeButtonIcon = new StackPane();

@ -1,10 +1,12 @@
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Card;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
@ -16,10 +18,16 @@ import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ;
@ -45,13 +53,14 @@ public class CardPage extends OutlinePage {
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 \
brief overview or context of the information. The subheader and body sections \
provide more detailed content, while the footer may include additional actions \
or information."""
);
addNode(skeleton());
addSection("Usage", usageExample());
addSection("Elevation", elevationExample());
addSection("Subheader", subHeaderExample());
}
private Node skeleton() {
@ -76,7 +85,7 @@ public class CardPage extends OutlinePage {
box.setMaxWidth(500);
box.getChildren().setAll(
cellBuilder.apply("header"),
cellBuilder.apply("sub-header"),
cellBuilder.apply("subheader"),
body,
cellBuilder.apply("footer")
);
@ -133,7 +142,7 @@ public class CardPage extends OutlinePage {
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]. \
You can use the [i]Tiles[/i] as either a header or body for the [i]Card[/i]. \
Its also suitable for building more complex dialogs as well."""
);
@ -160,17 +169,95 @@ public class CardPage extends OutlinePage {
"This is a title",
"This is a description"
));
card2.setBody(new Label("This is content"));
card2.setBody(new Label("This is a 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]."""
To add the raised effect to the [i]Card[/i], use the [code]Styles.ELEVATED_N[/code] \
or [code]Styles.INTERACTIVE[/code] style classes."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox subHeaderExample() {
//snippet_3:start
var card1 = new Card();
card1.getStyleClass().add(Styles.ELEVATED_1);
card1.setMinWidth(300);
card1.setMaxWidth(300);
var avatar1 = new Image(
Resources.getResourceAsStream("images/avatars/avatar3.png")
);
var btn1 = new Button(null, new FontIcon(Feather.MORE_VERTICAL));
btn1.getStyleClass().addAll(Styles.BUTTON_CIRCLE, Styles.FLAT);
var header1 = new Tile(
"Title",
"This is a description",
new ImageView(avatar1)
);
header1.setAction(btn1);
card1.setHeader(header1);
var image1 = new WritableImage(
new Image(
Resources.getResourceAsStream("images/pattern.jpg")
).getPixelReader(), 0, 0, 298, 150
);
card1.setSubHeader(new ImageView(image1));
var text1 = new TextFlow(new Text(FAKER.lorem().sentence(15)));
text1.setMaxWidth(260);
card1.setBody(text1);
// ~
var card2 = new Card();
card2.getStyleClass().add(Styles.ELEVATED_1);
card2.setMinWidth(300);
card2.setMaxWidth(300);
var header2 = new Tile(
"Reviewers",
"Request up to 10 reviewers"
);
card2.setHeader(header2);
var tf2 = new CustomTextField();
tf2.setPromptText("Search people");
tf2.setLeft(new FontIcon(Material2MZ.SEARCH));
card2.setSubHeader(tf2);
var body2 = new VBox(10);
card2.setBody(body2);
for (int i = 0; i < 5; i++) {
var cb = new CheckBox();
var lbl = new Label(FAKER.name().fullName());
var circle = new Circle(
8, Color.web(FAKER.color().hex(true))
);
var row = new HBox(10, circle, cb, lbl);
row.setAlignment(Pos.CENTER_LEFT);
body2.getChildren().add(row);
}
//snippet_3:end
var box = new HBox(HGAP_20, card1, card2);
box.setPadding(new Insets(0, 0, 10, 0));
var description = BBCodeParser.createFormattedText("""
The subheader slot is an optional space for interactive controls. Use \
it to display a search field, filter menu, or local navigation component. \
It also has special support for the [i]ImageView[/i]. If you place an image \
inside the subheader, it will remove its horizontal padding."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
}

@ -339,11 +339,11 @@ public final class ModalPanePage extends OutlinePage {
VBox.setVgrow(ta, Priority.ALWAYS);
var content = new VBox(
20,
16,
new Tile("Example Dialog", FAKER.lorem().sentence(10)),
ta
);
content.setPadding(new Insets(20));
content.setPadding(new Insets(16));
dialog.addContent(content);
AnchorPane.setTopAnchor(content, 0d);
AnchorPane.setRightAnchor(content, 0d);

@ -7,12 +7,15 @@ $color-bg: -color-bg-default !default;
$color-border: -color-border-default !default;
$padding-x: 0.75em !default;
$padding-y: 1em !default;
$spacing: 10px !default;
$spacing: 1em !default;
$title-font-size: cfg.$font-title-4;
.card {
>.container {
-fx-background-color: $color-bg;
-fx-alignment: TOP_LEFT;
-fx-padding: $padding-y $padding-x $padding-y $padding-x;
-fx-padding: $padding-y 0 $padding-y 0;
-fx-spacing: $spacing;
-fx-border-color: $color-border;
@ -21,20 +24,23 @@ $spacing: 10px !default;
>.header {
-fx-alignment: TOP_LEFT;
-fx-padding: 0 $padding-x 0 $padding-x 0;
}
>.sub-header {
-fx-alignment: TOP_LEFT;
-fx-padding: 0 $padding-x 0 $padding-x 0;
}
>.body {
// double spacing for body
-fx-padding: $spacing 0 $spacing 0;
// double vertical spacing for body
-fx-padding: 0 $padding-x 0 $padding-x;
-fx-alignment: TOP_LEFT;
}
>.footer {
-fx-alignment: TOP_LEFT;
-fx-padding: 0 $padding-x 0 $padding-x 0;
}
@each $level, $radius in cfg.$elevation {
@ -46,16 +52,29 @@ $spacing: 10px !default;
&.interactive:hover {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive);
}
.tile {
// prevent double indentation
-fx-padding: 0;
-fx-background-radius: 0;
}
}
.edge-to-edge>.card {
&:has-image {
>.container >.sub-header {
-fx-padding: 0;
}
}
&.edge-to-edge>.container {
-fx-border-width: 0;
-fx-border-radius: 0;
-fx-effect: none;
}
.tile {
>.container {
// prevent double indentation
-fx-padding: 0;
-fx-background-radius: 0;
>.header >.title {
-fx-font-size: $title-font-size;
}
}
}
}

@ -46,5 +46,11 @@ $close-button-icon-size: 0.3em !default;
// prevent double indentation inside dialog
-fx-padding: 0;
-fx-background-radius: 0;
>.container {
// prevent double indentation
-fx-padding: 0;
-fx-background-radius: 0;
}
}
}