Add Message control

This commit is contained in:
mkpaz 2023-05-25 16:39:12 +04:00
parent 31aa45dc05
commit b7a753c8f9
8 changed files with 353 additions and 10 deletions

@ -0,0 +1,35 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.controls;
import javafx.scene.Node;
/**
* The Message is a component for displaying notifications or alerts
* and is specifically designed to grab the users attention.
* It is based on the Tile layout and shares its structure.
*/
public class Message extends Tile {
/**
* See {@link Tile#Tile()}.
*/
public Message() {
super(null, null, null);
}
/**
* See {@link Tile#Tile(String, String)}.
*/
public Message(String title, String subtitle) {
this(title, subtitle, null);
}
/**
* See {@link Tile#Tile(String, String, Node)}.
*/
public Message(String title, String subtitle, Node graphic) {
super(title, subtitle, graphic);
getStyleClass().add("message");
}
}

@ -16,7 +16,8 @@ import javafx.scene.layout.VBox;
public class TileSkin extends SkinBase<Tile> {
private static final PseudoClass FILLED = PseudoClass.getPseudoClass("filled");
private static final PseudoClass WITH_TITLE = PseudoClass.getPseudoClass("with-title");
private static final PseudoClass WITH_SUBTITLE = PseudoClass.getPseudoClass("with-subtitle");
protected final HBox root = new HBox();
protected final StackPane graphicSlot;
@ -38,7 +39,8 @@ public class TileSkin extends SkinBase<Tile> {
titleLbl = new Label(control.getTitle());
titleLbl.getStyleClass().add("title");
titleLbl.textProperty().bind(control.titleProperty());
titleLbl.setVisible(control.getTitle() != null);
titleLbl.setManaged(control.getTitle() != null);
subTitleLbl = new Label(control.getSubTitle());
subTitleLbl.setWrapText(true);
@ -53,17 +55,24 @@ 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);
root.pseudoClassStateChanged(WITH_TITLE, control.getTitle() != null);
registerChangeListener(control.titleProperty(), o -> {
var value = getSkinnable().getSubTitle();
titleLbl.setText(value);
titleLbl.setVisible(value != null);
titleLbl.setManaged(value != null);
root.pseudoClassStateChanged(WITH_TITLE, value != null);
});
root.pseudoClassStateChanged(WITH_SUBTITLE, control.getSubTitle() != null);
registerChangeListener(control.subTitleProperty(), o -> {
var value = getSkinnable().getSubTitle();
subTitleLbl.setText(value);
subTitleLbl.setVisible(value != null);
subTitleLbl.setManaged(value != null);
// header is considered to be filled when a subtitle is set
// because a tile without a title is nonsense
headerBox.pseudoClassStateChanged(FILLED, value != null);
root.pseudoClassStateChanged(WITH_SUBTITLE, value != null);
});
actionSlot = new StackPane();
@ -125,7 +134,7 @@ public class TileSkin extends SkinBase<Tile> {
@Override
public void dispose() {
titleLbl.textProperty().unbind();
unregisterChangeListeners(getSkinnable().titleProperty());
unregisterChangeListeners(getSkinnable().subTitleProperty());
getSkinnable().graphicProperty().removeListener(graphicSlotListener);
getSkinnable().actionProperty().removeListener(actionSlotListener);

@ -27,6 +27,7 @@ import atlantafx.sampler.page.components.InputGroupPage;
import atlantafx.sampler.page.components.ListViewPage;
import atlantafx.sampler.page.components.MenuBarPage;
import atlantafx.sampler.page.components.MenuButtonPage;
import atlantafx.sampler.page.components.MessagePage;
import atlantafx.sampler.page.components.ModalPanePage;
import atlantafx.sampler.page.components.PaginationPage;
import atlantafx.sampler.page.components.PopoverPage;
@ -184,6 +185,7 @@ public class MainModel {
var feedback = NavTree.Item.group("Feedback", new FontIcon(Material2OutlinedAL.CHAT_BUBBLE_OUTLINE));
feedback.getChildren().setAll(
NAV_TREE.get(DialogPage.class),
NAV_TREE.get(MessagePage.class),
NAV_TREE.get(ProgressIndicatorPage.class),
NAV_TREE.get(TooltipPage.class)
);
@ -289,6 +291,7 @@ public class MainModel {
MenuButtonPage.NAME,
MenuButtonPage.class, "SplitMenuButton")
);
map.put(MessagePage.class, NavTree.Item.page(MessagePage.NAME, MessagePage.class));
map.put(ModalPanePage.class, NavTree.Item.page(ModalPanePage.NAME, ModalPanePage.class));
map.put(PaginationPage.class, NavTree.Item.page(PaginationPage.NAME, PaginationPage.class));
map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class));

@ -0,0 +1,181 @@
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Message;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.Animations;
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 javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
public class MessagePage extends OutlinePage {
public static final String NAME = "Message";
@Override
public String getName() {
return NAME;
}
@Override
public URI getJavadocUri() {
return URI.create(String.format(AFX_JAVADOC_URI_TEMPLATE, "controls/" + getName()));
}
public MessagePage() {
super();
addPageHeader();
addFormattedText("""
The [i]Message[/i] is a component for displaying notifications or alerts \
and is specifically designed to grab the users attention. It is \
based on the [i]Tile[/i] layout and shares its structure."""
);
addSection("Usage", usageExample());
addSection("No Title", noTitleExample());
addSection("Interactive", interactiveExample());
addSection("Banner", bannerExample());
}
private Node usageExample() {
//snippet_1:start
var info = new Message(
"Info",
FAKER.lorem().sentence(5),
new FontIcon(Material2OutlinedAL.HELP_OUTLINE)
);
var success = new Message(
"Success",
FAKER.lorem().sentence(10),
new FontIcon(Material2OutlinedAL.CHECK_CIRCLE_OUTLINE)
);
success.getStyleClass().add(Styles.SUCCESS);
var warning = new Message(
"Warning",
FAKER.lorem().sentence(20),
new FontIcon(Material2OutlinedMZ.OUTLINED_FLAG)
);
warning.getStyleClass().add(Styles.WARNING);
var danger = new Message(
"Danger",
FAKER.lorem().sentence(25),
new FontIcon(Material2OutlinedAL.ERROR_OUTLINE)
);
danger.getStyleClass().add(Styles.DANGER);
//snippet_1:end
var box = new VBox(VGAP_10, info, success, warning, danger);
var description = BBCodeParser.createFormattedText("""
The default [i]Message[/i] type is "info", which corresponds to the \
accent color. Success, warning, and danger colors are also supported."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private Node noTitleExample() {
//snippet_2:start
var info1 = new Message(
null,
FAKER.lorem().sentence(5),
new FontIcon(Material2OutlinedAL.HELP_OUTLINE)
);
var info2 = new Message(
null,
FAKER.lorem().sentence(15)
);
var info3 = new Message(
null,
FAKER.lorem().sentence(50)
);
var btn = new Button("Done");
btn.getStyleClass().add(Styles.ACCENT);
info3.setAction(btn);
//snippet_2:end
var box = new VBox(VGAP_10, info1, info2, info3);
var description = BBCodeParser.createFormattedText("""
Unlike the [i]Tile[/i], the message title is optional. This example \
demonstrates various messages without a title."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private Node interactiveExample() {
//snippet_3:start
var msg = new Message(
"Success",
FAKER.lorem().sentence(25)
);
msg.getStyleClass().add(Styles.SUCCESS);
var btn = new Button("Undo");
btn.getStyleClass().addAll(Styles.SUCCESS);
msg.setAction(btn);
msg.setActionHandler(() -> Animations.flash(msg).playFromStart());
//snippet_3:end
var box = new VBox(msg);
box.setPadding(new Insets(0, 0, 5, 0));
var description = BBCodeParser.createFormattedText("""
A [i]Message[/i] can be made interactive by setting an action handler that may \
or may not be related to the action slot."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private Node bannerExample() {
//snippet_4:start
final var msg = new Message(
null,
FAKER.lorem().sentence(10)
);
msg.getStyleClass().addAll(
Styles.DANGER, Tweaks.EDGE_TO_EDGE
);
var closeBtn = new Button("Close");
closeBtn.getStyleClass().addAll(Styles.DANGER);
msg.setAction(closeBtn);
var showBannerBtn = new Button("Show banner");
showBannerBtn.setOnAction(e1 -> {
var parent = (BorderPane) getScene().lookup("#main");
parent.setTop(new VBox(msg));
closeBtn.setOnAction(e2 -> parent.setTop(null));
msg.setOpacity(0);
Animations.fadeInDown(msg, Duration.millis(350))
.playFromStart();
});
//snippet_4:end
var box = new VBox(showBannerBtn);
var description = BBCodeParser.createFormattedText("""
The [i]Message[/i] supports the [code]Tweaks.EDGE_TO_EDGE[/code] style class modifier, \
which can be used to create a fancy banner, for example."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
}

@ -17,6 +17,7 @@
@use "label";
@use "menu";
@use "menu-button";
@use "message";
@use "modal-pane";
@use "pagination";
@use "popover";

@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
@use "../settings/config" as cfg;
$color-bg-accent: -color-accent-subtle !default;
$color-fg-accent-primary: -color-accent-fg !default;
$color-fg-accent-secondary: -color-fg-default !default;
$color-border-accent: -color-accent-muted !default;
$color-border-accent-hover: -color-accent-emphasis !default;
$color-bg-success: -color-success-subtle !default;
$color-fg-success-primary: -color-success-fg !default;
$color-fg-success-secondary: -color-fg-default !default;
$color-border-success: -color-success-muted !default;
$color-border-success-hover: -color-success-emphasis !default;
$color-bg-warning: -color-warning-subtle !default;
$color-fg-warning-primary: -color-warning-fg !default;
$color-fg-warning-secondary: -color-fg-default !default;
$color-border-warning: -color-warning-muted !default;
$color-border-warning-hover: -color-warning-emphasis !default;
$color-bg-danger: -color-danger-subtle !default;
$color-fg-danger-primary: -color-danger-fg !default;
$color-fg-danger-secondary: -color-fg-default !default;
$color-border-danger: -color-danger-muted !default;
$color-border-danger-hover: -color-danger-emphasis !default;
.message {
-color-message-bg: $color-bg-accent;
-color-message-fg-primary: $color-fg-accent-primary;
-color-message-fg-secondary: $color-fg-accent-secondary;
-color-message-border: $color-border-accent;
-color-message-border-interactive: $color-border-accent-hover;
&.success {
-color-message-bg: $color-bg-success;
-color-message-fg-primary: $color-fg-success-primary;
-color-message-fg-secondary: $color-fg-success-secondary;
-color-message-border: $color-border-success;
-color-message-border-interactive: $color-border-success-hover;
}
&.warning {
-color-message-bg: $color-bg-warning;
-color-message-fg-primary: $color-fg-warning-primary;
-color-message-fg-secondary: $color-fg-warning-secondary;
-color-message-border: $color-border-warning;
-color-message-border-interactive: $color-border-warning-hover;
}
&.danger {
-color-message-bg: $color-bg-danger;
-color-message-fg-primary: $color-fg-danger-primary;
-color-message-fg-secondary: $color-fg-danger-secondary;
-color-message-border: $color-border-danger;
-color-message-border-interactive: $color-border-danger-hover;
}
>.tile {
-fx-background-color: -color-message-bg;
-fx-alignment: TOP_LEFT;
-fx-border-color: -color-message-border;
-fx-border-width: cfg.$border-width;
-fx-border-radius: cfg.$border-radius;
&:hover:interactive {
-fx-background-color: -color-message-bg;
-fx-border-color: -color-message-border-interactive;
}
&:with-title:with-subtitle {
-fx-alignment: TOP_LEFT;
}
&:with-title,
&:with-subtitle {
-fx-alignment: CENTER_LEFT;
}
>.graphic {
-fx-alignment: TOP_LEFT;
}
>.header {
>.title {
-fx-text-fill: -color-message-fg-primary;
}
>.subtitle {
-fx-text-fill: -color-message-fg-secondary;
}
}
>.action {
-fx-alignment: TOP_LEFT;
}
#{cfg.$font-icon-selector} {
-fx-icon-color: -color-message-fg-primary;
-fx-fill: -color-message-fg-primary;
-fx-icon-size: cfg.$icon-size-larger;
}
}
&.edge-to-edge > .tile {
-fx-border-width: 0;
-fx-border-radius: 0;
}
}

@ -31,8 +31,10 @@ $color-interactive: -color-bg-subtle !default;
>.subtitle {
-fx-text-fill: -color-fg-muted;
}
}
&:filled {
&:with-title:with-subtitle {
>.header {
-fx-spacing: 0.5em;
-fx-alignment: TOP_LEFT;
}

@ -36,7 +36,8 @@ $font-icon-selector: ".font-icon, .ikonli-font-icon";
$font-icon-selector-immediate: ">.font-icon, >.ikonli-font-icon";
// Ikonli doesn't support 'em'
$icon-size: 18px !default;
$icon-size: 18px !default;
$icon-size-larger: 24px !default;
// elevation (level : radius)
$elevation: (1: 2px, 2: 8px, 3: 16px, 4: 20px) !default;