Refactor and improve Tile-based controls
This commit is contained in:
parent
998bd69334
commit
34acefa8f8
@ -2,34 +2,93 @@
|
||||
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
public class Message extends TileBase {
|
||||
|
||||
/**
|
||||
* See {@link Tile#Tile()}.
|
||||
*/
|
||||
public Message() {
|
||||
super(null, null, null);
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Tile#Tile(String, String)}.
|
||||
*/
|
||||
public Message(String title, String subtitle) {
|
||||
this(title, subtitle, null);
|
||||
public Message(@Nullable @NamedArg("title") String title,
|
||||
@Nullable @NamedArg("description") String description) {
|
||||
this(title, description, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Tile#Tile(String, String, Node)}.
|
||||
*/
|
||||
public Message(String title, String subtitle, Node graphic) {
|
||||
super(title, subtitle, graphic);
|
||||
public Message(@Nullable String title,
|
||||
@Nullable String description,
|
||||
@Nullable Node graphic) {
|
||||
super(title, description, graphic);
|
||||
getStyleClass().add("message");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new MessageSkin(this);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* The property representing the message’s action handler. Setting an action handler
|
||||
* makes the message interactive or clickable. When a user clicks on the interactive
|
||||
* message, the specified action handler will be called.
|
||||
*/
|
||||
private final ObjectProperty<Runnable> actionHandler = new SimpleObjectProperty<>(this, "actionHandler");
|
||||
|
||||
public Runnable getActionHandler() {
|
||||
return actionHandler.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Runnable> actionHandlerProperty() {
|
||||
return actionHandler;
|
||||
}
|
||||
|
||||
public void setActionHandler(Runnable actionHandler) {
|
||||
this.actionHandler.set(actionHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the user specified close handler. Note that
|
||||
* if you have also specified the ModalPane instance or CSS selector, this
|
||||
* handler will be executed after the default close handler. Therefore, you
|
||||
* can use it to perform arbitrary actions on dialog close.
|
||||
*/
|
||||
protected final ObjectProperty<EventHandler<? super Event>> onClose =
|
||||
new SimpleObjectProperty<>(this, "onClose");
|
||||
|
||||
public EventHandler<? super Event> getOnClose() {
|
||||
return onClose.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<EventHandler<? super Event>> onCloseProperty() {
|
||||
return onClose;
|
||||
}
|
||||
|
||||
public void setOnClose(EventHandler<? super Event> onClose) {
|
||||
this.onClose.set(onClose);
|
||||
}
|
||||
}
|
||||
|
78
base/src/main/java/atlantafx/base/controls/MessageSkin.java
Normal file
78
base/src/main/java/atlantafx/base/controls/MessageSkin.java
Normal file
@ -0,0 +1,78 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
public class MessageSkin extends TileSkinBase<Message> {
|
||||
|
||||
private static final PseudoClass CLOSEABLE = PseudoClass.getPseudoClass("closeable");
|
||||
|
||||
protected final StackPane closeButton = new StackPane();
|
||||
protected final StackPane closeButtonIcon = new StackPane();
|
||||
|
||||
public MessageSkin(Message control) {
|
||||
super(control);
|
||||
|
||||
// ACTION
|
||||
|
||||
pseudoClassStateChanged(Styles.STATE_INTERACTIVE, control.getActionHandler() != null);
|
||||
registerChangeListener(
|
||||
control.actionHandlerProperty(),
|
||||
o -> pseudoClassStateChanged(Styles.STATE_INTERACTIVE, getSkinnable().getActionHandler() != null)
|
||||
);
|
||||
|
||||
root.setOnMouseClicked(e -> {
|
||||
if (getSkinnable().getActionHandler() != null) {
|
||||
getSkinnable().getActionHandler().run();
|
||||
}
|
||||
});
|
||||
|
||||
// CLOSE BUTTON
|
||||
|
||||
closeButton.getStyleClass().add("close-button");
|
||||
closeButton.getChildren().setAll(closeButtonIcon);
|
||||
closeButton.setOnMouseClicked(e -> handleClose());
|
||||
closeButton.setVisible(control.getOnClose() != null);
|
||||
closeButton.setManaged(control.getOnClose() != null);
|
||||
|
||||
closeButtonIcon.getStyleClass().add("icon");
|
||||
getChildren().add(closeButton);
|
||||
|
||||
pseudoClassStateChanged(CLOSEABLE, control.getOnClose() != null);
|
||||
registerChangeListener(control.onCloseProperty(), o -> {
|
||||
closeButton.setVisible(getSkinnable().getOnClose() != null);
|
||||
closeButton.setManaged(getSkinnable().getOnClose() != null);
|
||||
pseudoClassStateChanged(CLOSEABLE, getSkinnable().onCloseProperty() != null);
|
||||
});
|
||||
}
|
||||
|
||||
protected void handleClose() {
|
||||
if (getSkinnable().getOnClose() != null) {
|
||||
getSkinnable().getOnClose().handle(new Event(Event.ANY));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
if (closeButton.isManaged()) {
|
||||
var lb = closeButton.getLayoutBounds();
|
||||
layoutInArea(closeButton, w - lb.getWidth() - 5, 5, lb.getWidth(), lb.getHeight(), -1, HPos.RIGHT,
|
||||
VPos.TOP);
|
||||
}
|
||||
layoutInArea(root, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
unregisterChangeListeners(getSkinnable().actionHandlerProperty());
|
||||
unregisterChangeListeners(getSkinnable().onCloseProperty());
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -3,21 +3,30 @@
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Pane;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
final class SlotListener implements ChangeListener<Node> {
|
||||
|
||||
private static final PseudoClass FILLED = PseudoClass.getPseudoClass("filled");
|
||||
|
||||
private final Pane slot;
|
||||
private final @Nullable BiConsumer<Node, Boolean> onContentUpdate;
|
||||
|
||||
public SlotListener(Node slot) {
|
||||
public SlotListener(Pane slot) {
|
||||
this(slot, null);
|
||||
}
|
||||
|
||||
public SlotListener(Node slot, @Nullable BiConsumer<Node, Boolean> onContentUpdate) {
|
||||
Objects.requireNonNull(slot, "Slot cannot be null.");
|
||||
|
||||
this.onContentUpdate = onContentUpdate;
|
||||
|
||||
if (slot instanceof Pane pane) {
|
||||
this.slot = pane;
|
||||
} else {
|
||||
@ -35,5 +44,9 @@ final class SlotListener implements ChangeListener<Node> {
|
||||
slot.setVisible(val != null);
|
||||
slot.setManaged(val != null);
|
||||
slot.pseudoClassStateChanged(FILLED, val != null);
|
||||
|
||||
if (onContentUpdate != null) {
|
||||
onContentUpdate.accept(val, val != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,35 +5,31 @@ package atlantafx.base.controls;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A versatile container that can used in various contexts such as dialog headers,
|
||||
* list items, and cards. It can contain a graphic, a title, subtitle, and optional
|
||||
* list items, and cards. It can contain a graphic, a title, description, and optional
|
||||
* actions.
|
||||
*/
|
||||
public class Tile extends Control {
|
||||
public class Tile extends TileBase {
|
||||
|
||||
public Tile() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public Tile(@NamedArg("title") String title,
|
||||
@NamedArg("subTitle") String subTitle) {
|
||||
this(title, subTitle, null);
|
||||
public Tile(@Nullable @NamedArg("title") String title,
|
||||
@Nullable @NamedArg("description") String description) {
|
||||
this(title, description, null);
|
||||
}
|
||||
|
||||
public Tile(@NamedArg("title") String title,
|
||||
@NamedArg("subTitle") String subTitle,
|
||||
@NamedArg("graphic") Node graphic) {
|
||||
super();
|
||||
setTitle(title);
|
||||
setSubTitle(subTitle);
|
||||
setGraphic(graphic);
|
||||
public Tile(@Nullable String title,
|
||||
@Nullable String description,
|
||||
@Nullable Node graphic) {
|
||||
super(title, description, graphic);
|
||||
getStyleClass().add("tile");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,60 +41,6 @@ public class Tile extends Control {
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* The property representing the tile’s graphic node. It is commonly used
|
||||
* to add images or icons that are associated with the tile.
|
||||
*/
|
||||
private final ObjectProperty<Node> graphic = new SimpleObjectProperty<>(this, "graphic");
|
||||
|
||||
public Node getGraphic() {
|
||||
return graphic.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> graphicProperty() {
|
||||
return graphic;
|
||||
}
|
||||
|
||||
public void setGraphic(Node graphic) {
|
||||
this.graphic.set(graphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the tile’s title. Although it is not mandatory,
|
||||
* you typically would not want to have a tile without a title.
|
||||
*/
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the tile’s subtitle. This is usually an optional
|
||||
* text or a description.
|
||||
*/
|
||||
private final StringProperty subTitle = new SimpleStringProperty(this, "subTitle");
|
||||
|
||||
public String getSubTitle() {
|
||||
return subTitle.get();
|
||||
}
|
||||
|
||||
public StringProperty subTitleProperty() {
|
||||
return subTitle;
|
||||
}
|
||||
|
||||
public void setSubTitle(String subTitle) {
|
||||
this.subTitle.set(subTitle);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the tile’s action node. It is commonly used
|
||||
* to place an action controls that are associated with the tile.
|
||||
|
92
base/src/main/java/atlantafx/base/controls/TileBase.java
Normal file
92
base/src/main/java/atlantafx/base/controls/TileBase.java
Normal file
@ -0,0 +1,92 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public abstract class TileBase extends Control {
|
||||
|
||||
public TileBase() {
|
||||
this(null, null, null);
|
||||
}
|
||||
|
||||
public TileBase(@Nullable @NamedArg("title") String title,
|
||||
@Nullable @NamedArg("description") String description) {
|
||||
this(title, description, null);
|
||||
}
|
||||
|
||||
public TileBase(@Nullable String title,
|
||||
@Nullable String description,
|
||||
@Nullable Node graphic) {
|
||||
super();
|
||||
|
||||
setTitle(title);
|
||||
setDescription(description);
|
||||
setGraphic(graphic);
|
||||
getStyleClass().add("tile-base");
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* The property representing the tile’s graphic node. It is commonly used
|
||||
* to add images or icons that are associated with the tile.
|
||||
*/
|
||||
private final ObjectProperty<Node> graphic = new SimpleObjectProperty<>(this, "graphic");
|
||||
|
||||
public Node getGraphic() {
|
||||
return graphic.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> graphicProperty() {
|
||||
return graphic;
|
||||
}
|
||||
|
||||
public void setGraphic(Node graphic) {
|
||||
this.graphic.set(graphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the tile’s title. Although it is not mandatory,
|
||||
* you typically would not want to have a tile without a title.
|
||||
*/
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title.set(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* The property representing the tile’s description.
|
||||
*/
|
||||
private final StringProperty description = new SimpleStringProperty(this, "description");
|
||||
|
||||
public String getDescription() {
|
||||
return description.get();
|
||||
}
|
||||
|
||||
public StringProperty descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description.set(description);
|
||||
}
|
||||
}
|
@ -3,140 +3,30 @@
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
public class TileSkin extends SkinBase<Tile> {
|
||||
|
||||
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;
|
||||
protected final ChangeListener<Node> graphicSlotListener;
|
||||
protected final VBox headerBox;
|
||||
protected final Label titleLbl;
|
||||
protected final Label subTitleLbl;
|
||||
protected final StackPane actionSlot;
|
||||
protected final ChangeListener<Node> actionSlotListener;
|
||||
public class TileSkin extends TileSkinBase<Tile> {
|
||||
|
||||
public TileSkin(Tile control) {
|
||||
super(control);
|
||||
|
||||
graphicSlot = new StackPane();
|
||||
graphicSlot.getStyleClass().add("graphic");
|
||||
graphicSlotListener = new SlotListener(graphicSlot);
|
||||
control.graphicProperty().addListener(graphicSlotListener);
|
||||
graphicSlotListener.changed(control.graphicProperty(), null, control.getGraphic());
|
||||
|
||||
titleLbl = new Label(control.getTitle());
|
||||
titleLbl.getStyleClass().add("title");
|
||||
titleLbl.setVisible(control.getTitle() != null);
|
||||
titleLbl.setManaged(control.getTitle() != null);
|
||||
|
||||
subTitleLbl = new Label(control.getSubTitle());
|
||||
subTitleLbl.setWrapText(true);
|
||||
subTitleLbl.getStyleClass().add("subtitle");
|
||||
subTitleLbl.setVisible(control.getSubTitle() != null);
|
||||
subTitleLbl.setManaged(control.getSubTitle() != null);
|
||||
|
||||
headerBox = new VBox(titleLbl, subTitleLbl);
|
||||
headerBox.setFillWidth(true);
|
||||
headerBox.getStyleClass().add("header");
|
||||
HBox.setHgrow(headerBox, Priority.ALWAYS);
|
||||
headerBox.setMinHeight(Region.USE_COMPUTED_SIZE);
|
||||
headerBox.setPrefHeight(Region.USE_COMPUTED_SIZE);
|
||||
headerBox.setMaxHeight(Region.USE_COMPUTED_SIZE);
|
||||
|
||||
|
||||
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);
|
||||
root.pseudoClassStateChanged(WITH_SUBTITLE, value != null);
|
||||
});
|
||||
|
||||
actionSlot = new StackPane();
|
||||
actionSlot.getStyleClass().add("action");
|
||||
actionSlotListener = new SlotListener(actionSlot);
|
||||
control.actionProperty().addListener(actionSlotListener);
|
||||
actionSlotListener.changed(control.actionProperty(), null, control.getAction());
|
||||
|
||||
// use pref size for slots, or they will be resized
|
||||
// to the bare minimum due to Priority.ALWAYS
|
||||
graphicSlot.setMinWidth(Region.USE_PREF_SIZE);
|
||||
actionSlot.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
// label text wrapping inside VBox won't work without this
|
||||
subTitleLbl.setMaxWidth(Region.USE_PREF_SIZE);
|
||||
subTitleLbl.setMinHeight(Region.USE_PREF_SIZE);
|
||||
|
||||
// do not resize children or container won't restore
|
||||
// to its original size after expanding
|
||||
root.setFillHeight(false);
|
||||
|
||||
root.getStyleClass().add("tile");
|
||||
root.getChildren().setAll(graphicSlot, headerBox, actionSlot);
|
||||
|
||||
root.pseudoClassStateChanged(Styles.STATE_INTERACTIVE, control.getActionHandler() != null);
|
||||
pseudoClassStateChanged(Styles.STATE_INTERACTIVE, control.getActionHandler() != null);
|
||||
registerChangeListener(
|
||||
control.actionHandlerProperty(),
|
||||
o -> root.pseudoClassStateChanged(Styles.STATE_INTERACTIVE, getSkinnable().getActionHandler() != null)
|
||||
o -> pseudoClassStateChanged(Styles.STATE_INTERACTIVE, getSkinnable().getActionHandler() != null)
|
||||
);
|
||||
|
||||
root.setOnMouseClicked(e -> {
|
||||
if (control.getActionHandler() != null) {
|
||||
control.getActionHandler().run();
|
||||
if (getSkinnable().getActionHandler() != null) {
|
||||
getSkinnable().getActionHandler().run();
|
||||
}
|
||||
});
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
|
||||
protected double calcHeight() {
|
||||
var headerHeight = headerBox.getSpacing()
|
||||
+ headerBox.getInsets().getTop()
|
||||
+ headerBox.getInsets().getBottom()
|
||||
+ titleLbl.getBoundsInLocal().getHeight()
|
||||
+ (subTitleLbl.isManaged() ? subTitleLbl.getBoundsInLocal().getHeight() : 0);
|
||||
|
||||
return Math.max(Math.max(graphicSlot.getHeight(), actionSlot.getHeight()), headerHeight)
|
||||
+ root.getPadding().getTop()
|
||||
+ root.getPadding().getBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMinHeight(double width, double topInset, double rightInset,
|
||||
double bottomInset, double leftInset) {
|
||||
// change the control height when label changed its height due to text wrapping,
|
||||
// no other way to do that, all JavaFX containers completely ignore _the actual_
|
||||
// height of its children
|
||||
return calcHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
unregisterChangeListeners(getSkinnable().titleProperty());
|
||||
unregisterChangeListeners(getSkinnable().subTitleProperty());
|
||||
getSkinnable().graphicProperty().removeListener(graphicSlotListener);
|
||||
getSkinnable().actionProperty().removeListener(actionSlotListener);
|
||||
unregisterChangeListeners(getSkinnable().actionHandlerProperty());
|
||||
|
||||
|
144
base/src/main/java/atlantafx/base/controls/TileSkinBase.java
Normal file
144
base/src/main/java/atlantafx/base/controls/TileSkinBase.java
Normal file
@ -0,0 +1,144 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
package atlantafx.base.controls;
|
||||
|
||||
import atlantafx.base.util.BBCodeParser;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
public abstract class TileSkinBase<T extends TileBase> extends SkinBase<T> {
|
||||
|
||||
protected static final PseudoClass HAS_GRAPHIC = PseudoClass.getPseudoClass("has-graphic");
|
||||
protected static final PseudoClass HAS_TITLE = PseudoClass.getPseudoClass("has-title");
|
||||
protected static final PseudoClass HAS_DESCRIPTION = PseudoClass.getPseudoClass("has-description");
|
||||
protected static final PseudoClass HAS_ACTION = PseudoClass.getPseudoClass("has-action");
|
||||
|
||||
protected final HBox root = new HBox();
|
||||
protected final StackPane graphicSlot;
|
||||
protected final ChangeListener<Node> graphicSlotListener;
|
||||
protected final VBox headerBox;
|
||||
protected final Label titleLbl;
|
||||
protected final TextFlow descriptionText;
|
||||
protected final StackPane actionSlot;
|
||||
protected final ChangeListener<Node> actionSlotListener;
|
||||
|
||||
public TileSkinBase(T control) {
|
||||
super(control);
|
||||
|
||||
graphicSlot = new StackPane();
|
||||
graphicSlot.getStyleClass().add("graphic");
|
||||
graphicSlotListener = new SlotListener(
|
||||
graphicSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_GRAPHIC, active)
|
||||
);
|
||||
control.graphicProperty().addListener(graphicSlotListener);
|
||||
graphicSlotListener.changed(control.graphicProperty(), null, control.getGraphic());
|
||||
|
||||
titleLbl = new Label(control.getTitle());
|
||||
titleLbl.getStyleClass().add("title");
|
||||
titleLbl.setVisible(control.getTitle() != null);
|
||||
titleLbl.setManaged(control.getTitle() != null);
|
||||
|
||||
descriptionText = new TextFlow();
|
||||
descriptionText.getStyleClass().add("description");
|
||||
descriptionText.setVisible(control.getDescription() != null);
|
||||
descriptionText.setManaged(control.getDescription() != null);
|
||||
setDescriptionText();
|
||||
|
||||
headerBox = new VBox(titleLbl, descriptionText);
|
||||
headerBox.setFillWidth(true);
|
||||
headerBox.getStyleClass().add("header");
|
||||
HBox.setHgrow(headerBox, Priority.ALWAYS);
|
||||
headerBox.setMinHeight(Region.USE_COMPUTED_SIZE);
|
||||
headerBox.setPrefHeight(Region.USE_COMPUTED_SIZE);
|
||||
headerBox.setMaxHeight(Region.USE_COMPUTED_SIZE);
|
||||
|
||||
control.pseudoClassStateChanged(HAS_TITLE, control.getTitle() != null);
|
||||
registerChangeListener(control.titleProperty(), o -> {
|
||||
var value = getSkinnable().getDescription();
|
||||
titleLbl.setText(value);
|
||||
titleLbl.setVisible(value != null);
|
||||
titleLbl.setManaged(value != null);
|
||||
getSkinnable().pseudoClassStateChanged(HAS_TITLE, value != null);
|
||||
});
|
||||
|
||||
control.pseudoClassStateChanged(HAS_DESCRIPTION, control.getDescription() != null);
|
||||
registerChangeListener(control.descriptionProperty(), o -> {
|
||||
var value = getSkinnable().getDescription();
|
||||
setDescriptionText();
|
||||
descriptionText.setVisible(value != null);
|
||||
descriptionText.setManaged(value != null);
|
||||
getSkinnable().pseudoClassStateChanged(HAS_DESCRIPTION, value != null);
|
||||
});
|
||||
|
||||
actionSlot = new StackPane();
|
||||
actionSlot.getStyleClass().add("action");
|
||||
actionSlotListener = new SlotListener(
|
||||
actionSlot, (n, active) -> getSkinnable().pseudoClassStateChanged(HAS_ACTION, active)
|
||||
);
|
||||
|
||||
// use pref size for slots, or they will be resized
|
||||
// to the bare minimum due to Priority.ALWAYS
|
||||
graphicSlot.setMinWidth(Region.USE_PREF_SIZE);
|
||||
actionSlot.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
// label text wrapping inside VBox won't work without this
|
||||
descriptionText.setMaxWidth(Region.USE_PREF_SIZE);
|
||||
descriptionText.setMinHeight(Region.USE_PREF_SIZE);
|
||||
|
||||
// do not resize children or container won't restore
|
||||
// to its original size after expanding
|
||||
root.setFillHeight(false);
|
||||
|
||||
root.getChildren().setAll(graphicSlot, headerBox, actionSlot);
|
||||
root.getStyleClass().add("container");
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
|
||||
protected void setDescriptionText() {
|
||||
if (!descriptionText.getChildren().isEmpty()) {
|
||||
descriptionText.getChildren().clear();
|
||||
}
|
||||
if (getSkinnable().getDescription() != null && !getSkinnable().getDescription().isBlank()) {
|
||||
BBCodeParser.createLayout(getSkinnable().getDescription(), descriptionText);
|
||||
}
|
||||
}
|
||||
|
||||
protected double calcHeight() {
|
||||
var headerHeight = headerBox.getSpacing()
|
||||
+ headerBox.getInsets().getTop()
|
||||
+ headerBox.getInsets().getBottom()
|
||||
+ titleLbl.getBoundsInLocal().getHeight()
|
||||
+ (descriptionText.isManaged() ? descriptionText.getBoundsInLocal().getHeight() : 0);
|
||||
|
||||
return Math.max(Math.max(graphicSlot.getHeight(), actionSlot.getHeight()), headerHeight)
|
||||
+ root.getPadding().getTop()
|
||||
+ root.getPadding().getBottom();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMinHeight(double width, double topInset, double rightInset,
|
||||
double bottomInset, double leftInset) {
|
||||
// change the control height when label changed its height due to text wrapping,
|
||||
// no other way to do that, all JavaFX containers completely ignore _the actual_
|
||||
// height of its children
|
||||
return calcHeight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
unregisterChangeListeners(getSkinnable().titleProperty());
|
||||
unregisterChangeListeners(getSkinnable().descriptionProperty());
|
||||
getSkinnable().graphicProperty().removeListener(graphicSlotListener);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -22,4 +22,8 @@ public final class BrowseEvent extends Event {
|
||||
+ "uri=" + uri
|
||||
+ "} " + super.toString();
|
||||
}
|
||||
|
||||
public static void fire(String url) {
|
||||
Event.publish(new BrowseEvent(URI.create(url)));
|
||||
}
|
||||
}
|
||||
|
@ -37,4 +37,8 @@ public abstract class Event {
|
||||
+ "id=" + id
|
||||
+ '}';
|
||||
}
|
||||
|
||||
public static <E extends Event> void publish(E event) {
|
||||
DefaultEventBus.getInstance().publish(event);
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ public class CardPage extends OutlinePage {
|
||||
card1.setMaxWidth(250);
|
||||
card1.setHeader(new Tile(
|
||||
"This is a title",
|
||||
"This is a subtitle"
|
||||
"This is a description"
|
||||
));
|
||||
card1.setBody(new Label("This is content"));
|
||||
|
||||
@ -158,7 +158,7 @@ public class CardPage extends OutlinePage {
|
||||
card2.setMaxWidth(250);
|
||||
card2.setHeader(new Tile(
|
||||
"This is a title",
|
||||
"This is a subtitle"
|
||||
"This is a description"
|
||||
));
|
||||
card2.setBody(new Label("This is content"));
|
||||
//snippet_2:end
|
||||
|
@ -1,20 +1,20 @@
|
||||
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.base.controls.Message;
|
||||
import atlantafx.sampler.event.BrowseEvent;
|
||||
import atlantafx.sampler.page.ExampleBox;
|
||||
import atlantafx.sampler.page.OutlinePage;
|
||||
import atlantafx.sampler.page.Snippet;
|
||||
import java.net.URI;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
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;
|
||||
@ -38,27 +38,60 @@ public class MessagePage extends OutlinePage {
|
||||
|
||||
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."""
|
||||
The [i]Message[/i] is a component for displaying an important text 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, except it doesn't \
|
||||
provide the action slot."""
|
||||
);
|
||||
addSection("Usage", usageExample());
|
||||
addSection("No Title", noTitleExample());
|
||||
addSection("Intent", intentExample());
|
||||
addSection("Incomplete Header", incompleteHeaderExample());
|
||||
addSection("Interactive", interactiveExample());
|
||||
addSection("Banner", bannerExample());
|
||||
addSection("Closeable", closeableExample());
|
||||
}
|
||||
|
||||
private Node usageExample() {
|
||||
// won't work inside the snippet, because code
|
||||
// snippet use BBCode parse as well
|
||||
var url = "https://wikipedia.org/wiki/The_Elder_Scrolls_III:_Morrowind";
|
||||
var quote = FAKER.elderScrolls().quote()
|
||||
+ " \n[url=" + url + "]Learn more[/url]";
|
||||
|
||||
//snippet_1:start
|
||||
var msg = new Message(
|
||||
"Quote",
|
||||
quote,
|
||||
new FontIcon(Material2OutlinedAL.CHAT_BUBBLE_OUTLINE)
|
||||
);
|
||||
msg.addEventFilter(ActionEvent.ACTION, e -> {
|
||||
if (e.getTarget() instanceof Hyperlink link) {
|
||||
BrowseEvent.fire((String) link.getUserData());
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
//snippet_1:end
|
||||
|
||||
var box = new VBox(msg);
|
||||
var description = BBCodeParser.createFormattedText("""
|
||||
[i]Message[/i] does not have any mandatory properties. It supports text \
|
||||
wrapping and [i]BBCode[/i] formatted text, but only in the description field."""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 1), description);
|
||||
}
|
||||
|
||||
private Node intentExample() {
|
||||
//snippet_5:start
|
||||
var info = new Message(
|
||||
"Info",
|
||||
FAKER.lorem().sentence(5),
|
||||
FAKER.lorem().sentence(10),
|
||||
new FontIcon(Material2OutlinedAL.HELP_OUTLINE)
|
||||
);
|
||||
info.getStyleClass().add(Styles.ACCENT);
|
||||
|
||||
var success = new Message(
|
||||
"Success",
|
||||
FAKER.lorem().sentence(10),
|
||||
FAKER.lorem().sentence(15),
|
||||
new FontIcon(Material2OutlinedAL.CHECK_CIRCLE_OUTLINE)
|
||||
);
|
||||
success.getStyleClass().add(Styles.SUCCESS);
|
||||
@ -76,45 +109,39 @@ public class MessagePage extends OutlinePage {
|
||||
new FontIcon(Material2OutlinedAL.ERROR_OUTLINE)
|
||||
);
|
||||
danger.getStyleClass().add(Styles.DANGER);
|
||||
//snippet_1:end
|
||||
//snippet_5: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."""
|
||||
The [i]Message[/i] offers four severity levels that set a distinctive color. \
|
||||
To change the [i]Message[/i] intent, use the corresponding style class modifier."""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 1), description);
|
||||
return new ExampleBox(box, new Snippet(getClass(), 5), description);
|
||||
}
|
||||
|
||||
private Node noTitleExample() {
|
||||
private Node incompleteHeaderExample() {
|
||||
//snippet_2:start
|
||||
var info1 = new Message(
|
||||
null,
|
||||
FAKER.lorem().sentence(5),
|
||||
null,
|
||||
new FontIcon(Material2OutlinedAL.HELP_OUTLINE)
|
||||
);
|
||||
info1.getStyleClass().add(Styles.ACCENT);
|
||||
|
||||
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);
|
||||
info2.getStyleClass().add(Styles.ACCENT);
|
||||
//snippet_2:end
|
||||
|
||||
var box = new VBox(VGAP_10, info1, info2, info3);
|
||||
var box = new VBox(VGAP_10, info1, info2);
|
||||
var description = BBCodeParser.createFormattedText("""
|
||||
Unlike the [i]Tile[/i], the message title is optional. This example \
|
||||
demonstrates various messages without a title."""
|
||||
Both the title and description are completely optional, but one them has to be \
|
||||
specified in any case. Note that the styling changes depending on whether the [i]Message[/i] \
|
||||
has only a title, only a description, or both."""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 2), description);
|
||||
}
|
||||
|
||||
@ -128,57 +155,72 @@ public class MessagePage extends OutlinePage {
|
||||
|
||||
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."""
|
||||
A [i]Message[/i] can be made interactive by setting an action handler. \
|
||||
This allows to call any arbitrary action when the user clicks inside \
|
||||
the message container. For example, you could show an extended dialog or \
|
||||
trigger a notification panel to appear.."""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 3), description);
|
||||
}
|
||||
|
||||
private Node bannerExample() {
|
||||
private Node closeableExample() {
|
||||
//snippet_4:start
|
||||
final var msg = new Message(
|
||||
null,
|
||||
FAKER.lorem().sentence(10)
|
||||
var regular = new Message(
|
||||
"Regular",
|
||||
FAKER.lorem().sentence(5),
|
||||
new FontIcon(Material2OutlinedAL.CHAT_BUBBLE_OUTLINE)
|
||||
);
|
||||
msg.getStyleClass().addAll(
|
||||
Styles.DANGER, Tweaks.EDGE_TO_EDGE
|
||||
regular.setOnClose(e -> Animations.flash(regular).playFromStart());
|
||||
|
||||
var info = new Message(
|
||||
"Info",
|
||||
FAKER.lorem().sentence(10),
|
||||
new FontIcon(Material2OutlinedAL.HELP_OUTLINE)
|
||||
);
|
||||
info.getStyleClass().add(Styles.ACCENT);
|
||||
info.setOnClose(e -> Animations.flash(info).playFromStart());
|
||||
|
||||
var closeBtn = new Button("Close");
|
||||
closeBtn.getStyleClass().addAll(Styles.DANGER);
|
||||
msg.setAction(closeBtn);
|
||||
var success = new Message(
|
||||
"Success",
|
||||
FAKER.lorem().sentence(15),
|
||||
new FontIcon(Material2OutlinedAL.CHECK_CIRCLE_OUTLINE)
|
||||
);
|
||||
success.getStyleClass().add(Styles.SUCCESS);
|
||||
success.setOnClose(e -> Animations.flash(success).playFromStart());
|
||||
|
||||
var showBannerBtn = new Button("Show banner");
|
||||
showBannerBtn.setOnAction(e1 -> {
|
||||
var parent = (BorderPane) getScene().lookup("#main");
|
||||
var warning = new Message(
|
||||
"Warning",
|
||||
FAKER.lorem().sentence(20),
|
||||
new FontIcon(Material2OutlinedMZ.OUTLINED_FLAG)
|
||||
);
|
||||
warning.getStyleClass().add(Styles.WARNING);
|
||||
warning.setOnClose(e -> Animations.flash(warning).playFromStart());
|
||||
|
||||
parent.setTop(new VBox(msg));
|
||||
closeBtn.setOnAction(e2 -> parent.setTop(null));
|
||||
|
||||
msg.setOpacity(0);
|
||||
Animations.fadeInDown(msg, Duration.millis(350))
|
||||
.playFromStart();
|
||||
});
|
||||
var danger = new Message(
|
||||
"Danger",
|
||||
FAKER.lorem().sentence(25),
|
||||
new FontIcon(Material2OutlinedAL.ERROR_OUTLINE)
|
||||
);
|
||||
danger.getStyleClass().add(Styles.DANGER);
|
||||
danger.setOnClose(e -> Animations.flash(danger).playFromStart());
|
||||
//snippet_4:end
|
||||
|
||||
var box = new VBox(showBannerBtn);
|
||||
var box = new VBox(VGAP_10, regular, info, success, warning, danger);
|
||||
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."""
|
||||
You can make the [i]Message[/i] closeable by setting an appropriate message handler. \
|
||||
If the handler is set, the close button will appear in the top right corner of the \
|
||||
[i]Message[/i]. This handler should provide some logic for removing the [i]Message[/i] \
|
||||
from its parent container as no default implementation is provided."""
|
||||
);
|
||||
box.setPadding(new Insets(0, 0, 5, 0));
|
||||
|
||||
var example = new ExampleBox(box, new Snippet(getClass(), 4), description);
|
||||
example.setAllowDisable(false);
|
||||
|
||||
return example;
|
||||
return new ExampleBox(box, new Snippet(getClass(), 4), description);
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,27 @@
|
||||
package atlantafx.sampler.page.components;
|
||||
|
||||
import atlantafx.base.controls.PasswordTextField;
|
||||
import atlantafx.base.controls.Tile;
|
||||
import atlantafx.base.controls.ToggleSwitch;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.base.util.Animations;
|
||||
import atlantafx.base.util.BBCodeParser;
|
||||
import atlantafx.sampler.Resources;
|
||||
import atlantafx.base.controls.Tile;
|
||||
import atlantafx.sampler.event.BrowseEvent;
|
||||
import atlantafx.sampler.page.ExampleBox;
|
||||
import atlantafx.sampler.page.OutlinePage;
|
||||
import atlantafx.sampler.page.Snippet;
|
||||
import java.net.URI;
|
||||
import java.util.function.BiFunction;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Spinner;
|
||||
@ -28,6 +32,7 @@ import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
import org.kordamp.ikonli.material2.Material2AL;
|
||||
import org.kordamp.ikonli.material2.Material2OutlinedAL;
|
||||
|
||||
public class TilePage extends OutlinePage {
|
||||
@ -51,12 +56,13 @@ public class TilePage extends OutlinePage {
|
||||
addFormattedText("""
|
||||
The Tile is a versatile container that can used in various contexts \
|
||||
such as dialog headers, list items, and cards. It can contain a graphic, \
|
||||
a title, subtitle, and optional actions."""
|
||||
a title, description, and optional actions."""
|
||||
);
|
||||
addNode(skeleton());
|
||||
addSection("Usage", usageExample());
|
||||
addSection("Interactive", interactiveExample());
|
||||
addSection("Stacking", stackingExample());
|
||||
addSection("Incomplete Header", incompleteHeaderExample());
|
||||
}
|
||||
|
||||
private Node skeleton() {
|
||||
@ -79,7 +85,7 @@ public class TilePage extends OutlinePage {
|
||||
grid.setMaxWidth(600);
|
||||
grid.add(cellBuilder.apply("Graphic", Pos.CENTER), 0, 0, 1, GridPane.REMAINING);
|
||||
grid.add(cellBuilder.apply("Title", Pos.CENTER_LEFT), 1, 0, 1, 1);
|
||||
grid.add(cellBuilder.apply("Subtitle", Pos.CENTER_LEFT), 1, 1, 1, 1);
|
||||
grid.add(cellBuilder.apply("Description", Pos.CENTER_LEFT), 1, 1, 1, 1);
|
||||
grid.add(cellBuilder.apply("Action", Pos.CENTER), 2, 0, 1, GridPane.REMAINING);
|
||||
grid.getColumnConstraints().setAll(
|
||||
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
|
||||
@ -91,16 +97,25 @@ public class TilePage extends OutlinePage {
|
||||
}
|
||||
|
||||
private Node usageExample() {
|
||||
// won't work inside the snippet, because code
|
||||
// snippet use BBCode parse as well
|
||||
var url = "https://wikipedia.org/wiki/The_Elder_Scrolls_III:_Morrowind";
|
||||
var quote = FAKER.elderScrolls().quote()
|
||||
+ " \n[url=" + url + "]Learn more[/url]";
|
||||
|
||||
//snippet_1:start
|
||||
var tile1 = new Tile(
|
||||
"Title",
|
||||
FAKER.lorem().sentence(15)
|
||||
"Multiline Description",
|
||||
FAKER.lorem().sentence(50)
|
||||
);
|
||||
|
||||
var tile2 = new Tile(
|
||||
FAKER.name().fullName(),
|
||||
FAKER.elderScrolls().quote()
|
||||
);
|
||||
var tile2 = new Tile(FAKER.name().fullName(), quote);
|
||||
tile2.addEventFilter(ActionEvent.ACTION, e -> {
|
||||
if (e.getTarget() instanceof Hyperlink link) {
|
||||
BrowseEvent.fire((String) link.getUserData());
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
|
||||
var img = new ImageView(new Image(
|
||||
Resources.getResourceAsStream("images/avatars/avatar1.png")
|
||||
@ -121,9 +136,8 @@ public class TilePage extends OutlinePage {
|
||||
tile3
|
||||
);
|
||||
var description = BBCodeParser.createFormattedText("""
|
||||
[i]Tile[/i] does not have any mandatory properties, but you will not want \
|
||||
to use it without a title. Additionally, note that only the subtitle supports \
|
||||
text wrapping."""
|
||||
[i]Tile[/i] does not have any mandatory properties. It supports text \
|
||||
wrapping and [i]BBCode[/i] formatted text, but only in the description field."""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 1), description);
|
||||
@ -133,7 +147,7 @@ public class TilePage extends OutlinePage {
|
||||
//snippet_2:start
|
||||
var tile = new Tile(
|
||||
"Password",
|
||||
"Please enter your authentication password to unlock the content"
|
||||
"Please enter your authentication password"
|
||||
);
|
||||
|
||||
var tf = new PasswordTextField(null);
|
||||
@ -197,4 +211,28 @@ public class TilePage extends OutlinePage {
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 3), description);
|
||||
}
|
||||
|
||||
private Node incompleteHeaderExample() {
|
||||
//snippet_4:start
|
||||
var tile1 = new Tile("Go to the next screen", null);
|
||||
tile1.setAction(new FontIcon(Material2AL.ARROW_RIGHT));
|
||||
tile1.setActionHandler(() ->
|
||||
Animations.wobble(tile1).playFromStart()
|
||||
);
|
||||
|
||||
var tile2 = new Tile(
|
||||
null, FAKER.friends().quote(),
|
||||
new FontIcon(Material2OutlinedAL.FORMAT_QUOTE)
|
||||
);
|
||||
//snippet_4:end
|
||||
|
||||
var box = new VBox(tile1, new Separator(), tile2);
|
||||
var description = BBCodeParser.createFormattedText("""
|
||||
Both the title and description are completely optional, but one them has to be \
|
||||
specified in any case. Note that the styling changes depending on whether the [i]Tile[/i] \
|
||||
has only a title, only a description, or both.\""""
|
||||
);
|
||||
|
||||
return new ExampleBox(box, new Snippet(getClass(), 4), description);
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
<PasswordTextField layoutX="14.0" layoutY="546.0" prefHeight="36.0" prefWidth="264.0" text="password" />
|
||||
<Label layoutX="14.0" layoutY="517.0" style="-fx-text-fill: green;" text="PasswordTextField" />
|
||||
<RingProgressIndicator layoutX="326.0" layoutY="25.0" progress="0.35" />
|
||||
<Tile layoutX="320.0" layoutY="209.0" prefHeight="73.0" prefWidth="483.0" subTitle="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fermentum, quam eu pretium euismod, ipsum mauris interdum massa, at scelerisque nulla augue a nunc. Vivamus vehicula rhoncus est, ut placerat nulla pellentesque vel. Duis ac mattis sapien. " title="Title" />
|
||||
<Tile layoutX="320.0" layoutY="209.0" prefHeight="73.0" prefWidth="483.0" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla fermentum, quam eu pretium euismod, ipsum mauris interdum massa, at scelerisque nulla augue a nunc. Vivamus vehicula rhoncus est, ut placerat nulla pellentesque vel. Duis ac mattis sapien. " title="Title" />
|
||||
<InputGroup layoutX="526.0" layoutY="57.0">
|
||||
<children>
|
||||
<ToggleButton mnemonicParsing="false" text="Toggle1" />
|
||||
|
@ -583,7 +583,7 @@
|
||||
<content>
|
||||
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0">
|
||||
<children>
|
||||
<Tile layoutX="-56.0" layoutY="44.0" subTitle="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sagittis vehicula elit, ac dictum metus bibendum id. Integer elit purus, varius ac eros eu, convallis ultricies tellus." title="Title" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||
<Tile layoutX="-56.0" layoutY="44.0" description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sagittis vehicula elit, ac dictum metus bibendum id. Integer elit purus, varius ac eros eu, convallis ultricies tellus." title="Title" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" />
|
||||
</children>
|
||||
</AnchorPane>
|
||||
</content>
|
||||
|
@ -1,44 +1,71 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@use "../settings/config" as cfg;
|
||||
@use "../settings/icons";
|
||||
|
||||
$color-bg: -color-bg-subtle !default;
|
||||
$color-fg-primary: -color-fg-default !default;
|
||||
$color-fg-secondary: -color-fg-default !default;
|
||||
$color-border: -color-border-muted !default;
|
||||
$color-border-hover: -color-border-default !default;
|
||||
$color-button-bg-hover: -color-bg-default !default;
|
||||
|
||||
$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-button-accent-bg-hover: -color-accent-muted !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-button-success-bg-hover: -color-success-muted !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-button-warning-bg-hover: -color-warning-muted !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;
|
||||
$color-button-danger-bg-hover: -color-danger-muted !default;
|
||||
|
||||
$close-button-radius: 100px !default;
|
||||
$close-button-padding: 0.6em !default;
|
||||
$close-button-icon-size: 0.3em !default;
|
||||
|
||||
.message {
|
||||
|
||||
-color-message-bg: $color-bg;
|
||||
-color-message-fg-primary: $color-fg-primary;
|
||||
-color-message-fg-secondary: $color-fg-secondary;
|
||||
-color-message-border: $color-border;
|
||||
-color-message-button-hover: $color-button-bg-hover;
|
||||
-color-message-border-interactive: $color-border-hover;
|
||||
|
||||
&.accent {
|
||||
-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-button-hover: $color-button-accent-bg-hover;
|
||||
-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-button-hover: $color-button-success-bg-hover;
|
||||
-color-message-border-interactive: $color-border-success-hover;
|
||||
}
|
||||
|
||||
@ -47,6 +74,7 @@ $color-border-danger-hover: -color-danger-emphasis !default;
|
||||
-color-message-fg-primary: $color-fg-warning-primary;
|
||||
-color-message-fg-secondary: $color-fg-warning-secondary;
|
||||
-color-message-border: $color-border-warning;
|
||||
-color-message-button-hover: $color-button-warning-bg-hover;
|
||||
-color-message-border-interactive: $color-border-warning-hover;
|
||||
}
|
||||
|
||||
@ -55,46 +83,29 @@ $color-border-danger-hover: -color-danger-emphasis !default;
|
||||
-color-message-fg-primary: $color-fg-danger-primary;
|
||||
-color-message-fg-secondary: $color-fg-danger-secondary;
|
||||
-color-message-border: $color-border-danger;
|
||||
-color-message-button-hover: $color-button-danger-bg-hover;
|
||||
-color-message-border-interactive: $color-border-danger-hover;
|
||||
}
|
||||
|
||||
>.tile {
|
||||
&.tile-base {
|
||||
>.container {
|
||||
|
||||
-fx-background-color: -color-message-bg;
|
||||
-fx-alignment: TOP_LEFT;
|
||||
-fx-alignment: CENTER_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;
|
||||
>.description {
|
||||
Text {
|
||||
-fx-fill: -color-message-fg-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#{cfg.$font-icon-selector} {
|
||||
@ -104,8 +115,54 @@ $color-border-danger-hover: -color-danger-emphasis !default;
|
||||
}
|
||||
}
|
||||
|
||||
&.edge-to-edge > .tile {
|
||||
-fx-border-width: 0;
|
||||
-fx-border-radius: 0;
|
||||
&:hover:interactive {
|
||||
>.container {
|
||||
-fx-background-color: -color-message-bg;
|
||||
-fx-border-color: -color-message-border-interactive;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
}
|
||||
|
||||
&:has-title:has-description {
|
||||
>.container>.header {
|
||||
>.title {
|
||||
-fx-text-fill: -color-message-fg-primary;
|
||||
}
|
||||
|
||||
>.description {
|
||||
Text {
|
||||
-fx-fill: -color-message-fg-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CLOSE BUTTON
|
||||
|
||||
>.close-button {
|
||||
-fx-background-radius: $close-button-radius;
|
||||
-fx-padding: $close-button-padding;
|
||||
|
||||
>.icon {
|
||||
@include icons.get("close", true);
|
||||
-fx-background-color: -color-message-fg-primary;
|
||||
-fx-padding: $close-button-icon-size;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
-fx-background-color: -color-message-button-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:closeable {
|
||||
>.container {
|
||||
>.header {
|
||||
>.title,
|
||||
>.description {
|
||||
-fx-padding: 0 ($close-button-padding * 3) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,46 +3,50 @@
|
||||
@use "../settings/config" as cfg;
|
||||
|
||||
$color-interactive: -color-bg-subtle !default;
|
||||
$title-font-size: 1.05em !default; // semibold would be much better, but it's JavaFX after all
|
||||
|
||||
.tile {
|
||||
-fx-padding: 0.75em 1em 0.75em 1em;
|
||||
.tile-base {
|
||||
|
||||
>.container {
|
||||
-fx-padding: 1em;
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
-fx-background-radius: cfg.$border-radius;
|
||||
-fx-spacing: 1em;
|
||||
|
||||
&:hover:interactive {
|
||||
-fx-background-color: $color-interactive;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
|
||||
>.graphic {
|
||||
&:filled {
|
||||
-fx-padding: 0 1em 0 0;
|
||||
}
|
||||
}
|
||||
// >.graphic {}
|
||||
|
||||
>.header {
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
-fx-padding: 0;
|
||||
|
||||
>.title {
|
||||
-fx-font-size: cfg.$font-title-4;
|
||||
}
|
||||
|
||||
>.subtitle {
|
||||
-fx-text-fill: -color-fg-muted;
|
||||
-fx-font-size: $title-font-size;
|
||||
}
|
||||
}
|
||||
|
||||
&:with-title:with-subtitle {
|
||||
>.header {
|
||||
// >.action {}
|
||||
}
|
||||
|
||||
&:has-title:has-description {
|
||||
>.container >.header {
|
||||
-fx-spacing: 0.5em;
|
||||
-fx-alignment: TOP_LEFT;
|
||||
|
||||
>.description {
|
||||
Text {
|
||||
-fx-fill: -color-fg-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.action {
|
||||
&:filled {
|
||||
-fx-padding: 0 0 0 1em;
|
||||
.tile {
|
||||
|
||||
&:hover:interactive {
|
||||
>.container {
|
||||
-fx-background-color: $color-interactive;
|
||||
-fx-cursor: hand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user