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 // Default constructor
public Card() { public Card() {
super(); super();
getStyleClass().add("card");
} }
@Override @Override

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

@ -11,7 +11,7 @@ import javafx.scene.layout.StackPane;
public class MessageSkin extends TileSkinBase<Message> { 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 closeButton = new StackPane();
protected final StackPane closeButtonIcon = new StackPane(); protected final StackPane closeButtonIcon = new StackPane();

@ -1,10 +1,12 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.base.controls.Card; import atlantafx.base.controls.Card;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.Tile; import atlantafx.base.controls.Tile;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.ExampleBox; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet; import atlantafx.sampler.page.Snippet;
@ -16,10 +18,16 @@ import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.Label; 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.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
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 org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ; import org.kordamp.ikonli.material2.Material2MZ;
@ -45,13 +53,14 @@ public class CardPage extends OutlinePage {
addFormattedText(""" addFormattedText("""
The [i]Card[/i] is a versatile container that can used in various contexts \ 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 \ 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 \ provide more detailed content, while the footer may include additional actions \
or information.""" or information."""
); );
addNode(skeleton()); addNode(skeleton());
addSection("Usage", usageExample()); addSection("Usage", usageExample());
addSection("Elevation", elevationExample()); addSection("Elevation", elevationExample());
addSection("Subheader", subHeaderExample());
} }
private Node skeleton() { private Node skeleton() {
@ -76,7 +85,7 @@ public class CardPage extends OutlinePage {
box.setMaxWidth(500); box.setMaxWidth(500);
box.getChildren().setAll( box.getChildren().setAll(
cellBuilder.apply("header"), cellBuilder.apply("header"),
cellBuilder.apply("sub-header"), cellBuilder.apply("subheader"),
body, body,
cellBuilder.apply("footer") cellBuilder.apply("footer")
); );
@ -133,7 +142,7 @@ public class CardPage extends OutlinePage {
var box = new HBox(HGAP_20, tweetCard, dialogCard); var box = new HBox(HGAP_20, tweetCard, dialogCard);
var description = BBCodeParser.createFormattedText(""" var description = BBCodeParser.createFormattedText("""
The [i]Card[/i] pairs well with the [i]Tile[/i] component. \ 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.""" 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 title",
"This is a description" "This is a description"
)); ));
card2.setBody(new Label("This is content")); card2.setBody(new Label("This is a content"));
//snippet_2:end //snippet_2:end
var box = new HBox(HGAP_20, card1, card2); var box = new HBox(HGAP_20, card1, card2);
box.setPadding(new Insets(0, 0, 10, 0)); box.setPadding(new Insets(0, 0, 10, 0));
var description = BBCodeParser.createFormattedText(""" var description = BBCodeParser.createFormattedText("""
With [code]Styles.ELEVATED_N[/code] or [code]Styles.INTERACTIVE[/code] styles classes \ To add the raised effect to the [i]Card[/i], use the [code]Styles.ELEVATED_N[/code] \
you can add raised shadow effect to the [i]Card[/i].""" or [code]Styles.INTERACTIVE[/code] style classes."""
); );
return new ExampleBox(box, new Snippet(getClass(), 2), description); 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); VBox.setVgrow(ta, Priority.ALWAYS);
var content = new VBox( var content = new VBox(
20, 16,
new Tile("Example Dialog", FAKER.lorem().sentence(10)), new Tile("Example Dialog", FAKER.lorem().sentence(10)),
ta ta
); );
content.setPadding(new Insets(20)); content.setPadding(new Insets(16));
dialog.addContent(content); dialog.addContent(content);
AnchorPane.setTopAnchor(content, 0d); AnchorPane.setTopAnchor(content, 0d);
AnchorPane.setRightAnchor(content, 0d); AnchorPane.setRightAnchor(content, 0d);

@ -7,55 +7,74 @@ $color-bg: -color-bg-default !default;
$color-border: -color-border-default !default; $color-border: -color-border-default !default;
$padding-x: 0.75em !default; $padding-x: 0.75em !default;
$padding-y: 1em !default; $padding-y: 1em !default;
$spacing: 10px !default; $spacing: 1em !default;
$title-font-size: cfg.$font-title-4;
.card { .card {
-fx-background-color: $color-bg; >.container {
-fx-alignment: TOP_LEFT; -fx-background-color: $color-bg;
-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; -fx-alignment: TOP_LEFT;
} -fx-padding: $padding-y 0 $padding-y 0;
-fx-spacing: $spacing;
>.sub-header { -fx-border-color: $color-border;
-fx-alignment: TOP_LEFT; -fx-border-width: cfg.$border-width;
} -fx-border-radius: cfg.$border-radius;
>.body { >.header {
// double spacing for body -fx-alignment: TOP_LEFT;
-fx-padding: $spacing 0 $spacing 0; -fx-padding: 0 $padding-x 0 $padding-x 0;
-fx-alignment: TOP_LEFT; }
}
>.footer { >.sub-header {
-fx-alignment: TOP_LEFT; -fx-alignment: TOP_LEFT;
} -fx-padding: 0 $padding-x 0 $padding-x 0;
}
@each $level, $radius in cfg.$elevation { >.body {
&.elevated-#{$level} { // double vertical spacing for body
@include effects.shadow(cfg.$elevation-color, $radius); -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 {
&.elevated-#{$level} {
@include effects.shadow(cfg.$elevation-color, $radius);
}
}
&.interactive:hover {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive);
} }
} }
&.interactive:hover { &:has-image {
@include effects.shadow(cfg.$elevation-color, cfg.$elevation-interactive); >.container >.sub-header {
-fx-padding: 0;
}
}
&.edge-to-edge>.container {
-fx-border-width: 0;
-fx-border-radius: 0;
-fx-effect: none;
} }
.tile { .tile {
// prevent double indentation >.container {
-fx-padding: 0; // prevent double indentation
-fx-background-radius: 0; -fx-padding: 0;
-fx-background-radius: 0;
>.header >.title {
-fx-font-size: $title-font-size;
}
}
} }
} }
.edge-to-edge>.card {
-fx-border-width: 0;
-fx-border-radius: 0;
-fx-effect: none;
}

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