diff --git a/base/src/main/java/atlantafx/base/layout/InputGroup.java b/base/src/main/java/atlantafx/base/layout/InputGroup.java new file mode 100644 index 0000000..33c3423 --- /dev/null +++ b/base/src/main/java/atlantafx/base/layout/InputGroup.java @@ -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); + } + } + } +} diff --git a/base/src/test/java/atlantafx/base/layout/InputGroupTest.java b/base/src/test/java/atlantafx/base/layout/InputGroupTest.java new file mode 100644 index 0000000..fe54d9d --- /dev/null +++ b/base/src/test/java/atlantafx/base/layout/InputGroupTest.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java index 6b8d54b..b07dfde 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainModel.java @@ -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( diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java b/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java index 1e8a7d5..6aae1b5 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/InputGroupPage.java @@ -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); diff --git a/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java b/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java index 77cb737..932e93a 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/components/TabPanePage.java @@ -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 ==