Add Message control
This commit is contained in:
parent
31aa45dc05
commit
b7a753c8f9
35
base/src/main/java/atlantafx/base/controls/Message.java
Normal file
35
base/src/main/java/atlantafx/base/controls/Message.java
Normal file
@ -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 user’s 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 user’s 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";
|
||||
|
111
styles/src/components/_message.scss
Normal file
111
styles/src/components/_message.scss
Normal file
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user