Add InputGroup layout

This commit is contained in:
mkpaz 2023-05-26 08:28:12 +04:00
parent baac4d9153
commit e86c95af29
5 changed files with 193 additions and 44 deletions

@ -0,0 +1,65 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.base.layout;
import atlantafx.base.theme.Styles;
import javafx.beans.InvalidationListener;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
/**
* InputGroup is a layout that helps combine multiple controls into a group
* that looks like a single control. Without it, you would have to manually
* add the "left-pill", "center-pill," and "right-pill" styles classes to
* each control in such combination. The InputGroup removes this ceremony.
* Since it inherits from HBox, you can use the same API.
*/
public class InputGroup extends HBox {
/**
* See {@link HBox#HBox()}.
*/
public InputGroup() {
super();
init();
}
/**
* See {@link HBox#HBox(Node...)}.
*/
public InputGroup(Node... children) {
super(children);
init();
}
protected void init() {
setAlignment(Pos.CENTER_LEFT);
getStyleClass().add("input-group");
updateStyles();
getChildren().addListener((InvalidationListener) o -> updateStyles());
}
// We don't clean up style classes if a control is removed from the input group.
// However, they will be fixed if the same control is added to the input group again.
protected void updateStyles() {
for (int i = 0; i < getChildren().size(); i++) {
Node n = getChildren().get(i);
n.getStyleClass().removeAll(
Styles.LEFT_PILL, Styles.CENTER_PILL, Styles.RIGHT_PILL
);
if (i == getChildren().size() - 1) {
if (i != 0) {
n.getStyleClass().add(Styles.RIGHT_PILL);
}
} else if (i == 0) {
n.getStyleClass().add(Styles.LEFT_PILL);
} else {
n.getStyleClass().add(Styles.CENTER_PILL);
}
}
}
}

@ -0,0 +1,97 @@
package atlantafx.base.layout;
import atlantafx.base.theme.Styles;
import javafx.scene.layout.Pane;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public class InputGroupTest {
@Test
public void testInitSingleNode() {
var g = new InputGroup(
new Pane()
);
Assertions.assertThat(g.getChildren().size()).isEqualTo(1);
Assertions.assertThat(g.getChildren().get(0).getStyleClass()).isEmpty();
}
@Test
public void testInitTwoNodes() {
var g = new InputGroup(
new Pane(), new Pane()
);
Assertions.assertThat(g.getChildren().size()).isEqualTo(2);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.RIGHT_PILL);
}
@Test
public void testInitMultipleNodes() {
var g = new InputGroup(
new Pane(), new Pane(), new Pane(), new Pane()
);
Assertions.assertThat(g.getChildren().size()).isEqualTo(4);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.CENTER_PILL);
assertStyle(g, 2, Styles.CENTER_PILL);
assertStyle(g, 3, Styles.RIGHT_PILL);
}
@Test
public void testAddNodes() {
var g = new InputGroup();
Assertions.assertThat(g.getChildren()).isEmpty();
g.getChildren().add(new Pane());
Assertions.assertThat(g.getChildren().size()).isEqualTo(1);
Assertions.assertThat(g.getChildren().get(0).getStyleClass()).isEmpty();
g.getChildren().add(new Pane());
Assertions.assertThat(g.getChildren().size()).isEqualTo(2);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.RIGHT_PILL);
g.getChildren().add(new Pane());
g.getChildren().add(new Pane());
Assertions.assertThat(g.getChildren().size()).isEqualTo(4);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.CENTER_PILL);
assertStyle(g, 2, Styles.CENTER_PILL);
assertStyle(g, 3, Styles.RIGHT_PILL);
}
@Test
public void testRemoveNodes() {
var g = new InputGroup(
new Pane(), new Pane(), new Pane(), new Pane()
);
Assertions.assertThat(g.getChildren().size()).isEqualTo(4);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.CENTER_PILL);
assertStyle(g, 2, Styles.CENTER_PILL);
assertStyle(g, 3, Styles.RIGHT_PILL);
g.getChildren().remove(0);
Assertions.assertThat(g.getChildren().size()).isEqualTo(3);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.CENTER_PILL);
assertStyle(g, 2, Styles.RIGHT_PILL);
g.getChildren().remove(0);
Assertions.assertThat(g.getChildren().size()).isEqualTo(2);
assertStyle(g, 0, Styles.LEFT_PILL);
assertStyle(g, 1, Styles.RIGHT_PILL);
g.getChildren().remove(0);
Assertions.assertThat(g.getChildren().size()).isEqualTo(1);
Assertions.assertThat(g.getChildren().get(0).getStyleClass()).isEmpty();
}
private void assertStyle(InputGroup g, int index, String style) {
Assertions.assertThat(g.getChildren().get(index).getStyleClass()).containsExactly(style);
}
}

@ -163,6 +163,7 @@ public class MainModel {
NAV_TREE.get(CardPage.class),
NAV_TREE.get(ContextMenuPage.class),
NAV_TREE.get(DeckPanePage.class),
NAV_TREE.get(InputGroupPage.class),
NAV_TREE.get(ModalPanePage.class),
NAV_TREE.get(ScrollPanePage.class),
NAV_TREE.get(SeparatorPage.class),
@ -200,7 +201,6 @@ public class MainModel {
NAV_TREE.get(ComboBoxPage.class),
NAV_TREE.get(CustomTextFieldPage.class),
NAV_TREE.get(DatePickerPage.class),
NAV_TREE.get(InputGroupPage.class),
NAV_TREE.get(HtmlEditorPage.class),
NAV_TREE.get(MenuButtonPage.class),
NAV_TREE.get(RadioButtonPage.class),
@ -259,7 +259,6 @@ public class MainModel {
);
// components
map.put(InputGroupPage.class, NavTree.Item.page(InputGroupPage.NAME, InputGroupPage.class));
map.put(AccordionPage.class, NavTree.Item.page(AccordionPage.NAME, AccordionPage.class));
map.put(BreadcrumbsPage.class, NavTree.Item.page(BreadcrumbsPage.NAME, BreadcrumbsPage.class));
map.put(ButtonPage.class, NavTree.Item.page(ButtonPage.NAME, ButtonPage.class));
@ -285,6 +284,7 @@ public class MainModel {
map.put(DeckPanePage.class, NavTree.Item.page(DeckPanePage.NAME, DeckPanePage.class));
map.put(DialogPage.class, NavTree.Item.page(DialogPage.NAME, DialogPage.class));
map.put(HtmlEditorPage.class, NavTree.Item.page(HtmlEditorPage.NAME, HtmlEditorPage.class));
map.put(InputGroupPage.class, NavTree.Item.page(InputGroupPage.NAME, InputGroupPage.class));
map.put(ListViewPage.class, NavTree.Item.page(ListViewPage.NAME, ListViewPage.class));
map.put(MenuBarPage.class, NavTree.Item.page(MenuBarPage.NAME, MenuBarPage.class));
map.put(MenuButtonPage.class, NavTree.Item.page(

@ -2,6 +2,7 @@
package atlantafx.sampler.page.components;
import atlantafx.base.layout.InputGroup;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
@ -19,13 +20,12 @@ import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public final class InputGroupPage extends OutlinePage {
public static final String NAME = "Input Group";
public static final String NAME = "InputGroup";
@Override
public String getName() {
@ -33,8 +33,8 @@ public final class InputGroupPage extends OutlinePage {
}
@Override
public @Nullable URI getJavadocUri() {
return null;
public URI getJavadocUri() {
return URI.create(String.format(AFX_JAVADOC_URI_TEMPLATE, "layout/" + getName()));
}
public InputGroupPage() {
@ -42,10 +42,12 @@ public final class InputGroupPage extends OutlinePage {
addPageHeader();
addFormattedText("""
You can use the following utility classes: [code]Styles.LEFT_PILL[/code], \
[code]Styles.CENTER_PILL[/code], and [code]Styles.RIGHT_PILL[/code] to combine \
various input controls into input groups that allow them to appear as a single \
control. This is entirely a CSS feature and does not require any additional wrappers."""
[i]InputGroup[/i] is a layout that helps combine various controls into a group \
that allow them to appear as a single control. Without it, you would have \
to manually add the [font=monospace].left-pill[/font], [font=monospace].center-pill[/font], \
and [font=monospace].right-pill[/font] styles classes to each control in such combination. \
You still can, but the [i]InputGroup[/i] removes this ceremony. And since it inherits \
from [i]HBox[/i], you can use the same API."""
);
addSection("ComboBox", comboBoxExample());
addSection("Button", buttonExample());
@ -58,16 +60,15 @@ public final class InputGroupPage extends OutlinePage {
//snippet_1:start
var leftCmb = new ComboBox<>();
leftCmb.getItems().addAll("POST", "GET", "PUT", "PATCH", "DELETE");
leftCmb.getStyleClass().add(Styles.LEFT_PILL);
leftCmb.getSelectionModel().selectFirst();
var rightTfd = new TextField("https://example.org");
rightTfd.getStyleClass().add(Styles.RIGHT_PILL);
HBox.setHgrow(rightTfd, Priority.ALWAYS);
var group = new InputGroup(leftCmb, rightTfd);
//snippet_1:end
var box = new HBox(leftCmb, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
var box = new HBox(group);
box.setMinWidth(400);
box.setMaxWidth(400);
@ -82,7 +83,6 @@ public final class InputGroupPage extends OutlinePage {
//snippet_2:start
var leftTfd = new TextField();
leftTfd.setText(FAKER.internet().password());
leftTfd.getStyleClass().add(Styles.LEFT_PILL);
HBox.setHgrow(leftTfd, Priority.ALWAYS);
var rightBtn = new Button(
@ -92,11 +92,11 @@ public final class InputGroupPage extends OutlinePage {
rightBtn.setOnAction(
e -> leftTfd.setText(FAKER.internet().password())
);
rightBtn.getStyleClass().add(Styles.RIGHT_PILL);
var group = new InputGroup(leftTfd, rightBtn);
//snippet_2:end
var box = new HBox(leftTfd, rightBtn);
box.setAlignment(Pos.CENTER_LEFT);
var box = new HBox(group);
box.setMinWidth(400);
box.setMaxWidth(400);
@ -110,18 +110,16 @@ public final class InputGroupPage extends OutlinePage {
private ExampleBox textFieldExample() {
//snippet_3:start
var leftTfd = new TextField("192.168.1.10");
leftTfd.getStyleClass().add(Styles.LEFT_PILL);
var centerTfd = new TextField("24");
centerTfd.getStyleClass().add(Styles.CENTER_PILL);
centerTfd.setPrefWidth(70);
var rightTfd = new TextField("192.168.1.1");
rightTfd.getStyleClass().add(Styles.RIGHT_PILL);
var group = new InputGroup(leftTfd, centerTfd, rightTfd);
//snippet_3:end
var box = new HBox(leftTfd, centerTfd, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
var box = new HBox(group);
box.setMinWidth(400);
box.setMaxWidth(400);
@ -135,7 +133,6 @@ public final class InputGroupPage extends OutlinePage {
private ExampleBox menuButtonExample() {
//snippet_4:start
var rightTfd = new TextField(FAKER.harryPotter().spell());
rightTfd.getStyleClass().add(Styles.RIGHT_PILL);
HBox.setHgrow(rightTfd, Priority.ALWAYS);
var spellItem = new MenuItem("Spell");
@ -155,11 +152,11 @@ public final class InputGroupPage extends OutlinePage {
var leftMenu = new MenuButton("Dropdown");
leftMenu.getItems().addAll(spellItem, characterItem, locationItem);
leftMenu.getStyleClass().add(Styles.LEFT_PILL);
var group = new InputGroup(leftMenu, rightTfd);
//snippet_4:end
var box = new HBox(leftMenu, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
var box = new HBox(group);
box.setMinWidth(400);
box.setMaxWidth(400);
@ -173,43 +170,33 @@ public final class InputGroupPage extends OutlinePage {
private ExampleBox labelExample() {
//snippet_5:start
var leftLbl1 = new Label("", new CheckBox());
leftLbl1.getStyleClass().add(Styles.LEFT_PILL);
var rightTfd1 = new TextField();
rightTfd1.setPromptText("Username");
rightTfd1.getStyleClass().add(Styles.RIGHT_PILL);
HBox.setHgrow(rightTfd1, Priority.ALWAYS);
var sample1 = new HBox(leftLbl1, rightTfd1);
sample1.setAlignment(Pos.CENTER_LEFT);
var sample1 = new InputGroup(leftLbl1, rightTfd1);
// ~
var leftTfd2 = new TextField("johndoe");
leftTfd2.getStyleClass().add(Styles.LEFT_PILL);
HBox.setHgrow(leftTfd2, Priority.ALWAYS);
var centerLbl2 = new Label("@");
centerLbl2.setMinWidth(50);
centerLbl2.setAlignment(Pos.CENTER);
centerLbl2.getStyleClass().add(Styles.CENTER_PILL);
var rightTfd2 = new TextField("gmail.com");
rightTfd2.getStyleClass().add(Styles.RIGHT_PILL);
HBox.setHgrow(rightTfd2, Priority.ALWAYS);
var sample2 = new HBox(leftTfd2, centerLbl2, rightTfd2);
sample2.setAlignment(Pos.CENTER_LEFT);
var sample2 = new InputGroup(leftTfd2, centerLbl2, rightTfd2);
// ~
var leftTfd3 = new TextField("+123456");
leftTfd3.getStyleClass().add(Styles.LEFT_PILL);
HBox.setHgrow(leftTfd3, Priority.ALWAYS);
var rightLbl3 = new Label("", new FontIcon(Feather.DOLLAR_SIGN));
rightLbl3.getStyleClass().add(Styles.RIGHT_PILL);
var sample3 = new HBox(leftTfd3, rightLbl3);
sample3.setAlignment(Pos.CENTER_LEFT);
var sample3 = new InputGroup(leftTfd3, rightLbl3);
//snippet_5:end
sample1.setMinWidth(400);

@ -6,6 +6,7 @@ import static javafx.scene.control.TabPane.TabClosingPolicy;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.layout.InputGroup;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
@ -346,7 +347,6 @@ public final class TabPanePage extends OutlinePage {
defaultStyleToggle.setUserData(
List.of("whatever", Styles.TABS_FLOATING, Styles.TABS_CLASSIC)
);
defaultStyleToggle.getStyleClass().add(Styles.LEFT_PILL);
defaultStyleToggle.setSelected(true);
var floatingStyleToggle = new ToggleButton("Floating");
@ -354,14 +354,12 @@ public final class TabPanePage extends OutlinePage {
floatingStyleToggle.setUserData(
List.of(Styles.TABS_FLOATING, "whatever", Styles.TABS_CLASSIC)
);
floatingStyleToggle.getStyleClass().add(Styles.CENTER_PILL);
var classicStyleToggle = new ToggleButton("Classic");
classicStyleToggle.setToggleGroup(styleToggleGroup);
classicStyleToggle.setUserData(
List.of(Styles.TABS_CLASSIC, "whatever", Styles.TABS_FLOATING)
);
classicStyleToggle.getStyleClass().add(Styles.RIGHT_PILL);
styleToggleGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null) {
@ -370,7 +368,9 @@ public final class TabPanePage extends OutlinePage {
}
});
var styleBox = new HBox(defaultStyleToggle, floatingStyleToggle, classicStyleToggle);
var styleBox = new InputGroup(
defaultStyleToggle, floatingStyleToggle, classicStyleToggle
);
styleBox.setAlignment(Pos.CENTER);
// == LAYOUT ==