Rewrite all Sampler pages to better UI design

This commit is contained in:
mkpaz 2023-05-02 14:29:31 +04:00
parent 7ebfa7ac0e
commit 35d1107b3d
137 changed files with 10032 additions and 7489 deletions

@ -184,8 +184,9 @@ public class ModalPane extends Control {
/** /**
* Specifies whether content should be treated as persistent or not. * Specifies whether content should be treated as persistent or not.
* By default, modal pane exits when on ESC button or mouse click outside the contenbt are. * By default, the modal pane exits when the ESC button is pressed or when
* This property prevents this behavior and plays bouncing animation instead. * the mouse is clicked outside the content area. This property prevents
* this behavior and plays bouncing animation instead.
*/ */
public BooleanProperty persistentProperty() { public BooleanProperty persistentProperty() {
return persistent; return persistent;

@ -141,4 +141,4 @@ public class PasswordTextFormatterTest {
assertEquals("+".repeat(5), field.getText()); assertEquals("+".repeat(5), field.getText());
assertEquals("123++", fmt.getPassword()); assertEquals("123++", fmt.getPassword());
} }
} }

@ -94,6 +94,11 @@
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
<version>${openjfx.version}</version> <version>${openjfx.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${openjfx.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId> <artifactId>javafx-swing</artifactId>

@ -43,6 +43,10 @@
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId> <artifactId>javafx-controls</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.openjfx</groupId> <groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId> <artifactId>javafx-swing</artifactId>
@ -162,7 +166,7 @@
<executions> <executions>
<execution> <execution>
<id>validate</id> <id>validate</id>
<phase>validate</phase> <phase>verify</phase>
<goals> <goals>
<goal>check</goal> <goal>check</goal>
</goals> </goals>
@ -243,7 +247,7 @@
<configuration> <configuration>
<toolName>jlink</toolName> <toolName>jlink</toolName>
<addModules> <addModules>
java.base,java.logging,jdk.localedata,java.desktop,java.prefs,javafx.controls,javafx.swing,javafx.web java.base,java.logging,jdk.localedata,java.desktop,java.prefs,javafx.controls,javafx.fxml,javafx.swing,javafx.web
</addModules> </addModules>
<modulePath>${build.platformModulesDir}</modulePath> <modulePath>${build.platformModulesDir}</modulePath>
<output>${build.package.runtimeImageDir}</output> <output>${build.package.runtimeImageDir}</output>

@ -59,7 +59,7 @@ public class Launcher extends Application {
var antialiasing = Platform.isSupported(ConditionalFeature.SCENE3D) var antialiasing = Platform.isSupported(ConditionalFeature.SCENE3D)
? SceneAntialiasing.BALANCED ? SceneAntialiasing.BALANCED
: SceneAntialiasing.DISABLED; : SceneAntialiasing.DISABLED;
var scene = new Scene(root, 1200, 768, false, antialiasing); var scene = new Scene(root, ApplicationWindow.MIN_WIDTH + 80, 768, false, antialiasing);
scene.setOnKeyPressed(this::dispatchHotkeys); scene.setOnKeyPressed(this::dispatchHotkeys);
var tm = ThemeManager.getInstance(); var tm = ThemeManager.getInstance();

@ -1,156 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.fake;
import static atlantafx.sampler.util.Controls.menuItem;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
import atlantafx.base.controls.CaptionMenuItem;
import java.util.stream.IntStream;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import net.datafaker.Faker;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
public class SampleMenuBar extends MenuBar {
private static final EventHandler<ActionEvent> PRINT_SOURCE = System.out::println;
public SampleMenuBar(Faker faker) {
getMenus().addAll(
fileMenu(faker),
editMenu(),
viewMenu(),
toolsMenu(),
aboutMenu()
);
}
private Menu fileMenu(Faker faker) {
var fileMenu = new Menu("_File");
fileMenu.setMnemonicParsing(true);
fileMenu.setOnAction(PRINT_SOURCE);
var newMenu = menuItem("_New", null, new KeyCodeCombination(KeyCode.N, CONTROL_DOWN));
newMenu.setMnemonicParsing(true);
newMenu.setOnAction(PRINT_SOURCE);
var openRecentMenu = new Menu("Open _Recent");
openRecentMenu.setMnemonicParsing(true);
openRecentMenu.setOnAction(PRINT_SOURCE);
openRecentMenu.getItems().addAll(
IntStream.range(0, 10).mapToObj(x -> new MenuItem(faker.file().fileName())).toList()
);
fileMenu.getItems().addAll(
newMenu,
new SeparatorMenuItem(),
menuItem("Open", Feather.FOLDER, new KeyCodeCombination(KeyCode.O, CONTROL_DOWN)),
openRecentMenu,
new SeparatorMenuItem(),
menuItem("Save", Feather.SAVE, new KeyCodeCombination(KeyCode.S, CONTROL_DOWN)),
new MenuItem("Save As"),
new SeparatorMenuItem(),
new MenuItem("Exit")
);
return fileMenu;
}
private Menu editMenu() {
var editMenu = new Menu("_Edit");
editMenu.setMnemonicParsing(true);
editMenu.setOnAction(PRINT_SOURCE);
editMenu.getItems().addAll(
menuItem("Undo", Feather.CORNER_DOWN_LEFT, new KeyCodeCombination(KeyCode.Z, CONTROL_DOWN)),
menuItem("Redo", Feather.CORNER_DOWN_RIGHT, new KeyCodeCombination(KeyCode.Y, CONTROL_DOWN)),
new SeparatorMenuItem(),
menuItem("Cut", Feather.SCISSORS, new KeyCodeCombination(KeyCode.X, CONTROL_DOWN)),
menuItem("Copy", Feather.COPY, new KeyCodeCombination(KeyCode.C, CONTROL_DOWN), true),
menuItem("Paste", Feather.CORNER_DOWN_LEFT, new KeyCodeCombination(KeyCode.V, CONTROL_DOWN))
);
return editMenu;
}
private Menu viewMenu() {
var viewMenu = new Menu("_View");
viewMenu.setMnemonicParsing(true);
viewMenu.setOnAction(PRINT_SOURCE);
var showToolbarItem = new CheckMenuItem("Show Toolbar", new FontIcon(Feather.TOOL));
showToolbarItem.setSelected(true);
showToolbarItem.setAccelerator(new KeyCodeCombination(KeyCode.T, CONTROL_DOWN));
var showGridItem = new CheckMenuItem("Show Grid", new FontIcon(Feather.GRID));
var captionItem = new CaptionMenuItem("Layout");
var viewToggleGroup = new ToggleGroup();
var toggleItem1 = new RadioMenuItem("Single", new FontIcon(Material2OutlinedAL.LOOKS_ONE));
toggleItem1.setSelected(true);
toggleItem1.setToggleGroup(viewToggleGroup);
var toggleItem2 = new RadioMenuItem("Two Columns", new FontIcon(Material2OutlinedAL.LOOKS_TWO));
toggleItem2.setToggleGroup(viewToggleGroup);
var toggleItem3 = new RadioMenuItem("Three Columns", new FontIcon(Material2OutlinedAL.LOOKS_3));
toggleItem3.setToggleGroup(viewToggleGroup);
viewMenu.getItems().addAll(
showToolbarItem,
showGridItem,
new SeparatorMenuItem(),
captionItem,
toggleItem1,
toggleItem2,
toggleItem3
);
return viewMenu;
}
private Menu toolsMenu() {
var toolsMenu = new Menu("_Tools");
toolsMenu.setMnemonicParsing(true);
toolsMenu.setOnAction(PRINT_SOURCE);
toolsMenu.setDisable(true);
return toolsMenu;
}
private Menu aboutMenu() {
var aboutMenu = new Menu("_About", new FontIcon(Feather.HELP_CIRCLE));
aboutMenu.setMnemonicParsing(true);
aboutMenu.setOnAction(PRINT_SOURCE);
var deeplyNestedMenu = new Menu("Very...", null,
new Menu("Very...", null,
new Menu("Deeply", null,
new Menu("Nested", null,
new MenuItem("Menu")
))));
// NOTE: this won't be displayed because right container is reserved for submenu indication
deeplyNestedMenu.setAccelerator(new KeyCodeCombination(
KeyCode.DIGIT1, SHIFT_DOWN, CONTROL_DOWN)
);
aboutMenu.getItems().addAll(
new MenuItem("Help"),
new MenuItem("About"),
new SeparatorMenuItem(),
deeplyNestedMenu
);
return aboutMenu;
}
}

@ -0,0 +1,61 @@
package atlantafx.sampler.fake.domain;
import javafx.beans.property.SimpleStringProperty;
public final class Metric {
private final SimpleStringProperty queries = new SimpleStringProperty("");
private final SimpleStringProperty cacheHitRate = new SimpleStringProperty("");
private final SimpleStringProperty latency = new SimpleStringProperty("");
private final SimpleStringProperty requests = new SimpleStringProperty("");
public Metric() { }
public String getQueries() {
return queries.get();
}
public SimpleStringProperty queriesProperty() {
return queries;
}
public void setQueries(String queries) {
this.queries.set(queries);
}
public String getCacheHitRate() {
return cacheHitRate.get();
}
public SimpleStringProperty cacheHitRateProperty() {
return cacheHitRate;
}
public void setCacheHitRate(String cacheHitRate) {
this.cacheHitRate.set(cacheHitRate);
}
public String getLatency() {
return latency.get();
}
public SimpleStringProperty latencyProperty() {
return latency;
}
public void setLatency(String latency) {
this.latency.set(latency);
}
public String getRequests() {
return requests.get();
}
public SimpleStringProperty requestsProperty() {
return requests;
}
public void setRequests(String requests) {
this.requests.set(requests);
}
}

@ -9,6 +9,9 @@ import javafx.scene.layout.StackPane;
public class ApplicationWindow extends AnchorPane { public class ApplicationWindow extends AnchorPane {
public static final int MIN_WIDTH = 1200;
public static final int SIDEBAR_WIDTH = 250;
public ApplicationWindow() { public ApplicationWindow() {
// this is the place to apply user custom CSS, // this is the place to apply user custom CSS,
// one level below the ':root' // one level below the ':root'

@ -6,7 +6,6 @@ import static atlantafx.base.theme.Styles.TEXT_SMALL;
import static atlantafx.base.theme.Styles.TITLE_3; import static atlantafx.base.theme.Styles.TITLE_3;
import static atlantafx.base.theme.Styles.TITLE_4; import static atlantafx.base.theme.Styles.TITLE_4;
import static atlantafx.sampler.Launcher.IS_DEV_MODE; import static atlantafx.sampler.Launcher.IS_DEV_MODE;
import static atlantafx.sampler.layout.MainLayer.SIDEBAR_WIDTH;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
@ -80,9 +79,9 @@ class HeaderBar extends HBox {
var logoBox = new HBox(10, logoImageBox, logoLabel, versionLabel); var logoBox = new HBox(10, logoImageBox, logoLabel, versionLabel);
logoBox.getStyleClass().add("logo"); logoBox.getStyleClass().add("logo");
logoBox.setAlignment(Pos.CENTER_LEFT); logoBox.setAlignment(Pos.CENTER_LEFT);
logoBox.setMinWidth(SIDEBAR_WIDTH); logoBox.setMinWidth(ApplicationWindow.SIDEBAR_WIDTH);
logoBox.setPrefWidth(SIDEBAR_WIDTH); logoBox.setPrefWidth(ApplicationWindow.SIDEBAR_WIDTH);
logoBox.setMaxWidth(SIDEBAR_WIDTH); logoBox.setMaxWidth(ApplicationWindow.SIDEBAR_WIDTH);
var titleLabel = new Label(); var titleLabel = new Label();
titleLabel.getStyleClass().addAll("page-title", TITLE_4); titleLabel.getStyleClass().addAll("page-title", TITLE_4);

@ -26,7 +26,6 @@ import javafx.util.Duration;
class MainLayer extends BorderPane { class MainLayer extends BorderPane {
static final int SIDEBAR_WIDTH = 250;
static final int PAGE_TRANSITION_DURATION = 500; // ms static final int PAGE_TRANSITION_DURATION = 500; // ms
private final MainModel model = new MainModel(); private final MainModel model = new MainModel();
@ -51,8 +50,8 @@ class MainLayer extends BorderPane {
} }
private void createView() { private void createView() {
sidebar.setMinWidth(SIDEBAR_WIDTH); sidebar.setMinWidth(ApplicationWindow.SIDEBAR_WIDTH);
sidebar.setMaxWidth(SIDEBAR_WIDTH); sidebar.setMaxWidth(ApplicationWindow.SIDEBAR_WIDTH);
codeViewer = new CodeViewer(); codeViewer = new CodeViewer();

@ -7,27 +7,22 @@ import static atlantafx.sampler.layout.MainModel.SubLayer.SOURCE_CODE;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.AccordionPage; import atlantafx.sampler.page.components.AccordionPage;
import atlantafx.sampler.page.components.BBCodePage;
import atlantafx.sampler.page.components.BreadcrumbsPage;
import atlantafx.sampler.page.components.ButtonPage; import atlantafx.sampler.page.components.ButtonPage;
import atlantafx.sampler.page.components.ChartPage; import atlantafx.sampler.page.components.ChartPage;
import atlantafx.sampler.page.components.CheckBoxPage; import atlantafx.sampler.page.components.CheckBoxPage;
import atlantafx.sampler.page.components.ChoiceBoxPage;
import atlantafx.sampler.page.components.ColorPickerPage; import atlantafx.sampler.page.components.ColorPickerPage;
import atlantafx.sampler.page.components.ComboBoxPage; import atlantafx.sampler.page.components.ComboBoxPage;
import atlantafx.sampler.page.components.CustomTextFieldPage; import atlantafx.sampler.page.components.ContextMenuPage;
import atlantafx.sampler.page.components.DatePickerPage; import atlantafx.sampler.page.components.DatePickerPage;
import atlantafx.sampler.page.components.DialogPage; import atlantafx.sampler.page.components.DialogPage;
import atlantafx.sampler.page.components.HtmlEditorPage; import atlantafx.sampler.page.components.HtmlEditorPage;
import atlantafx.sampler.page.components.InputGroupPage;
import atlantafx.sampler.page.components.LabelPage; import atlantafx.sampler.page.components.LabelPage;
import atlantafx.sampler.page.components.ListPage; import atlantafx.sampler.page.components.ListViewPage;
import atlantafx.sampler.page.components.MenuBarPage;
import atlantafx.sampler.page.components.MenuButtonPage; import atlantafx.sampler.page.components.MenuButtonPage;
import atlantafx.sampler.page.components.MenuPage;
import atlantafx.sampler.page.components.ModalPanePage;
import atlantafx.sampler.page.components.OverviewPage;
import atlantafx.sampler.page.components.PaginationPage; import atlantafx.sampler.page.components.PaginationPage;
import atlantafx.sampler.page.components.PopoverPage; import atlantafx.sampler.page.components.ProgressIndicatorPage;
import atlantafx.sampler.page.components.ProgressPage;
import atlantafx.sampler.page.components.RadioButtonPage; import atlantafx.sampler.page.components.RadioButtonPage;
import atlantafx.sampler.page.components.ScrollPanePage; import atlantafx.sampler.page.components.ScrollPanePage;
import atlantafx.sampler.page.components.SeparatorPage; import atlantafx.sampler.page.components.SeparatorPage;
@ -35,22 +30,30 @@ import atlantafx.sampler.page.components.SliderPage;
import atlantafx.sampler.page.components.SpinnerPage; import atlantafx.sampler.page.components.SpinnerPage;
import atlantafx.sampler.page.components.SplitPanePage; import atlantafx.sampler.page.components.SplitPanePage;
import atlantafx.sampler.page.components.TabPanePage; import atlantafx.sampler.page.components.TabPanePage;
import atlantafx.sampler.page.components.TablePage; import atlantafx.sampler.page.components.TableViewPage;
import atlantafx.sampler.page.components.TextAreaPage; import atlantafx.sampler.page.components.TextAreaPage;
import atlantafx.sampler.page.components.TextFieldPage; import atlantafx.sampler.page.components.TextFieldPage;
import atlantafx.sampler.page.components.TitledPanePage; import atlantafx.sampler.page.components.TitledPanePage;
import atlantafx.sampler.page.components.ToggleButtonPage; import atlantafx.sampler.page.components.ToggleButtonPage;
import atlantafx.sampler.page.components.ToggleSwitchPage;
import atlantafx.sampler.page.components.ToolBarPage; import atlantafx.sampler.page.components.ToolBarPage;
import atlantafx.sampler.page.components.TooltipPage; import atlantafx.sampler.page.components.TooltipPage;
import atlantafx.sampler.page.components.TreePage; import atlantafx.sampler.page.components.TreeTableViewPage;
import atlantafx.sampler.page.components.TreeTablePage; import atlantafx.sampler.page.components.TreeViewPage;
import atlantafx.sampler.page.extras.BBCodePage;
import atlantafx.sampler.page.extras.BreadcrumbsPage;
import atlantafx.sampler.page.extras.CalendarPage;
import atlantafx.sampler.page.extras.CustomTextFieldPage;
import atlantafx.sampler.page.extras.DeckPanePage;
import atlantafx.sampler.page.extras.InputGroupPage;
import atlantafx.sampler.page.extras.ModalPanePage;
import atlantafx.sampler.page.extras.PopoverPage;
import atlantafx.sampler.page.extras.ToggleSwitchPage;
import atlantafx.sampler.page.general.IconsPage; import atlantafx.sampler.page.general.IconsPage;
import atlantafx.sampler.page.general.OverviewPage;
import atlantafx.sampler.page.general.ThemePage; import atlantafx.sampler.page.general.ThemePage;
import atlantafx.sampler.page.general.TypographyPage; import atlantafx.sampler.page.general.TypographyPage;
import atlantafx.sampler.page.showcase.filemanager.FileManagerPage; import atlantafx.sampler.page.showcase.filemanager.FileManagerPage;
import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage; import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage;
import atlantafx.sampler.page.showcase.widget.WidgetCollectionPage;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -156,24 +159,26 @@ public class MainModel {
NAV_TREE.get(ButtonPage.class), NAV_TREE.get(ButtonPage.class),
NAV_TREE.get(ChartPage.class), NAV_TREE.get(ChartPage.class),
NAV_TREE.get(CheckBoxPage.class), NAV_TREE.get(CheckBoxPage.class),
NAV_TREE.get(ChoiceBoxPage.class),
NAV_TREE.get(ColorPickerPage.class), NAV_TREE.get(ColorPickerPage.class),
NAV_TREE.get(ComboBoxPage.class), NAV_TREE.get(ComboBoxPage.class),
NAV_TREE.get(ContextMenuPage.class),
NAV_TREE.get(DatePickerPage.class), NAV_TREE.get(DatePickerPage.class),
NAV_TREE.get(DialogPage.class), NAV_TREE.get(DialogPage.class),
NAV_TREE.get(HtmlEditorPage.class), NAV_TREE.get(HtmlEditorPage.class),
NAV_TREE.get(LabelPage.class), NAV_TREE.get(LabelPage.class),
NAV_TREE.get(ListPage.class), NAV_TREE.get(ListViewPage.class),
NAV_TREE.get(MenuPage.class), NAV_TREE.get(MenuBarPage.class),
NAV_TREE.get(MenuButtonPage.class), NAV_TREE.get(MenuButtonPage.class),
NAV_TREE.get(PaginationPage.class), NAV_TREE.get(PaginationPage.class),
NAV_TREE.get(ProgressPage.class), NAV_TREE.get(ProgressIndicatorPage.class),
NAV_TREE.get(RadioButtonPage.class), NAV_TREE.get(RadioButtonPage.class),
NAV_TREE.get(ScrollPanePage.class), NAV_TREE.get(ScrollPanePage.class),
NAV_TREE.get(SeparatorPage.class), NAV_TREE.get(SeparatorPage.class),
NAV_TREE.get(SliderPage.class), NAV_TREE.get(SliderPage.class),
NAV_TREE.get(SpinnerPage.class), NAV_TREE.get(SpinnerPage.class),
NAV_TREE.get(SplitPanePage.class), NAV_TREE.get(SplitPanePage.class),
NAV_TREE.get(TablePage.class), NAV_TREE.get(TableViewPage.class),
NAV_TREE.get(TabPanePage.class), NAV_TREE.get(TabPanePage.class),
NAV_TREE.get(TextAreaPage.class), NAV_TREE.get(TextAreaPage.class),
NAV_TREE.get(TextFieldPage.class), NAV_TREE.get(TextFieldPage.class),
@ -181,16 +186,18 @@ public class MainModel {
NAV_TREE.get(ToggleButtonPage.class), NAV_TREE.get(ToggleButtonPage.class),
NAV_TREE.get(ToolBarPage.class), NAV_TREE.get(ToolBarPage.class),
NAV_TREE.get(TooltipPage.class), NAV_TREE.get(TooltipPage.class),
NAV_TREE.get(TreePage.class), NAV_TREE.get(TreeTableViewPage.class),
NAV_TREE.get(TreeTablePage.class) NAV_TREE.get(TreeViewPage.class)
); );
var extras = NavTree.Item.group("Extras", new FontIcon(Material2OutlinedMZ.TOGGLE_ON)); var extras = NavTree.Item.group("Extras", new FontIcon(Material2OutlinedMZ.TOGGLE_ON));
extras.getChildren().setAll( extras.getChildren().setAll(
NAV_TREE.get(InputGroupPage.class),
NAV_TREE.get(BBCodePage.class), NAV_TREE.get(BBCodePage.class),
NAV_TREE.get(BreadcrumbsPage.class), NAV_TREE.get(BreadcrumbsPage.class),
NAV_TREE.get(CalendarPage.class),
NAV_TREE.get(CustomTextFieldPage.class), NAV_TREE.get(CustomTextFieldPage.class),
NAV_TREE.get(DeckPanePage.class),
NAV_TREE.get(InputGroupPage.class),
NAV_TREE.get(ModalPanePage.class), NAV_TREE.get(ModalPanePage.class),
NAV_TREE.get(PopoverPage.class), NAV_TREE.get(PopoverPage.class),
NAV_TREE.get(ToggleSwitchPage.class) NAV_TREE.get(ToggleSwitchPage.class)
@ -199,8 +206,7 @@ public class MainModel {
var showcases = NavTree.Item.group("Showcase", new FontIcon(Material2OutlinedMZ.VISIBILITY)); var showcases = NavTree.Item.group("Showcase", new FontIcon(Material2OutlinedMZ.VISIBILITY));
showcases.getChildren().setAll( showcases.getChildren().setAll(
NAV_TREE.get(FileManagerPage.class), NAV_TREE.get(FileManagerPage.class),
NAV_TREE.get(MusicPlayerPage.class), NAV_TREE.get(MusicPlayerPage.class)
NAV_TREE.get(WidgetCollectionPage.class)
); );
var root = NavTree.Item.root(); var root = NavTree.Item.root();
@ -216,33 +222,39 @@ public class MainModel {
public static Map<Class<? extends Page>, NavTree.Item> createNavItems() { public static Map<Class<? extends Page>, NavTree.Item> createNavItems() {
var map = new HashMap<Class<? extends Page>, NavTree.Item>(); var map = new HashMap<Class<? extends Page>, NavTree.Item>();
// general
map.put(OverviewPage.class, NavTree.Item.page(OverviewPage.NAME, OverviewPage.class)); map.put(OverviewPage.class, NavTree.Item.page(OverviewPage.NAME, OverviewPage.class));
map.put(ThemePage.class, NavTree.Item.page(ThemePage.NAME, ThemePage.class)); map.put(ThemePage.class, NavTree.Item.page(ThemePage.NAME, ThemePage.class));
map.put(TypographyPage.class, NavTree.Item.page(TypographyPage.NAME, TypographyPage.class)); map.put(TypographyPage.class, NavTree.Item.page(TypographyPage.NAME, TypographyPage.class));
map.put(IconsPage.class, NavTree.Item.page(IconsPage.NAME, IconsPage.class)); map.put(IconsPage.class, NavTree.Item.page(IconsPage.NAME, IconsPage.class));
// components
map.put(InputGroupPage.class, NavTree.Item.page(InputGroupPage.NAME, InputGroupPage.class)); map.put(InputGroupPage.class, NavTree.Item.page(InputGroupPage.NAME, InputGroupPage.class));
map.put(AccordionPage.class, NavTree.Item.page(AccordionPage.NAME, AccordionPage.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(BreadcrumbsPage.class, NavTree.Item.page(BreadcrumbsPage.NAME, BreadcrumbsPage.class));
map.put(ButtonPage.class, NavTree.Item.page(ButtonPage.NAME, ButtonPage.class)); map.put(ButtonPage.class, NavTree.Item.page(ButtonPage.NAME, ButtonPage.class));
map.put(BBCodePage.class, NavTree.Item.page(BBCodePage.NAME, BBCodePage.class)); map.put(BBCodePage.class, NavTree.Item.page(BBCodePage.NAME, BBCodePage.class));
map.put(CalendarPage.class, NavTree.Item.page(CalendarPage.NAME, CalendarPage.class));
map.put(ChartPage.class, NavTree.Item.page(ChartPage.NAME, ChartPage.class)); map.put(ChartPage.class, NavTree.Item.page(ChartPage.NAME, ChartPage.class));
map.put(ChoiceBoxPage.class, NavTree.Item.page(ChoiceBoxPage.NAME, ChoiceBoxPage.class));
map.put(CheckBoxPage.class, NavTree.Item.page(CheckBoxPage.NAME, CheckBoxPage.class)); map.put(CheckBoxPage.class, NavTree.Item.page(CheckBoxPage.NAME, CheckBoxPage.class));
map.put(ColorPickerPage.class, NavTree.Item.page(ColorPickerPage.NAME, ColorPickerPage.class)); map.put(ColorPickerPage.class, NavTree.Item.page(ColorPickerPage.NAME, ColorPickerPage.class));
map.put( map.put(
ComboBoxPage.class, ComboBoxPage.class,
NavTree.Item.page(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox") NavTree.Item.page(ComboBoxPage.NAME, ComboBoxPage.class, "ChoiceBox")
); );
map.put(ContextMenuPage.class, NavTree.Item.page(ContextMenuPage.NAME, ContextMenuPage.class));
map.put( map.put(
CustomTextFieldPage.class, CustomTextFieldPage.class,
NavTree.Item.page(CustomTextFieldPage.NAME, CustomTextFieldPage.class, "MaskTextField", "PasswordTextField") NavTree.Item.page(CustomTextFieldPage.NAME, CustomTextFieldPage.class, "MaskTextField", "PasswordTextField")
); );
map.put(DatePickerPage.class, NavTree.Item.page(DatePickerPage.NAME, DatePickerPage.class)); map.put(DatePickerPage.class, NavTree.Item.page(DatePickerPage.NAME, DatePickerPage.class));
map.put(DeckPanePage.class, NavTree.Item.page(DeckPanePage.NAME, DeckPanePage.class));
map.put(DialogPage.class, NavTree.Item.page(DialogPage.NAME, DialogPage.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(HtmlEditorPage.class, NavTree.Item.page(HtmlEditorPage.NAME, HtmlEditorPage.class));
map.put(LabelPage.class, NavTree.Item.page(LabelPage.NAME, LabelPage.class)); map.put(LabelPage.class, NavTree.Item.page(LabelPage.NAME, LabelPage.class));
map.put(ListPage.class, NavTree.Item.page(ListPage.NAME, ListPage.class)); map.put(ListViewPage.class, NavTree.Item.page(ListViewPage.NAME, ListViewPage.class));
map.put(MenuPage.class, NavTree.Item.page(MenuPage.NAME, MenuPage.class)); map.put(MenuBarPage.class, NavTree.Item.page(MenuBarPage.NAME, MenuBarPage.class));
map.put(MenuButtonPage.class, NavTree.Item.page( map.put(MenuButtonPage.class, NavTree.Item.page(
MenuButtonPage.NAME, MenuButtonPage.NAME,
MenuButtonPage.class, "SplitMenuButton") MenuButtonPage.class, "SplitMenuButton")
@ -250,14 +262,17 @@ public class MainModel {
map.put(ModalPanePage.class, NavTree.Item.page(ModalPanePage.NAME, ModalPanePage.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(PaginationPage.class, NavTree.Item.page(PaginationPage.NAME, PaginationPage.class));
map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class)); map.put(PopoverPage.class, NavTree.Item.page(PopoverPage.NAME, PopoverPage.class));
map.put(ProgressPage.class, NavTree.Item.page(ProgressPage.NAME, ProgressPage.class)); map.put(ProgressIndicatorPage.class, NavTree.Item.page(
ProgressIndicatorPage.NAME,
ProgressIndicatorPage.class, "ProgressBar")
);
map.put(RadioButtonPage.class, NavTree.Item.page(RadioButtonPage.NAME, RadioButtonPage.class)); map.put(RadioButtonPage.class, NavTree.Item.page(RadioButtonPage.NAME, RadioButtonPage.class));
map.put(ScrollPanePage.class, NavTree.Item.page(ScrollPanePage.NAME, ScrollPanePage.class)); map.put(ScrollPanePage.class, NavTree.Item.page(ScrollPanePage.NAME, ScrollPanePage.class));
map.put(SeparatorPage.class, NavTree.Item.page(SeparatorPage.NAME, SeparatorPage.class)); map.put(SeparatorPage.class, NavTree.Item.page(SeparatorPage.NAME, SeparatorPage.class));
map.put(SliderPage.class, NavTree.Item.page(SliderPage.NAME, SliderPage.class)); map.put(SliderPage.class, NavTree.Item.page(SliderPage.NAME, SliderPage.class));
map.put(SpinnerPage.class, NavTree.Item.page(SpinnerPage.NAME, SpinnerPage.class)); map.put(SpinnerPage.class, NavTree.Item.page(SpinnerPage.NAME, SpinnerPage.class));
map.put(SplitPanePage.class, NavTree.Item.page(SplitPanePage.NAME, SplitPanePage.class)); map.put(SplitPanePage.class, NavTree.Item.page(SplitPanePage.NAME, SplitPanePage.class));
map.put(TablePage.class, NavTree.Item.page(TablePage.NAME, TablePage.class)); map.put(TableViewPage.class, NavTree.Item.page(TableViewPage.NAME, TableViewPage.class));
map.put(TabPanePage.class, NavTree.Item.page(TabPanePage.NAME, TabPanePage.class)); map.put(TabPanePage.class, NavTree.Item.page(TabPanePage.NAME, TabPanePage.class));
map.put(TextAreaPage.class, NavTree.Item.page(TextAreaPage.NAME, TextAreaPage.class)); map.put(TextAreaPage.class, NavTree.Item.page(TextAreaPage.NAME, TextAreaPage.class));
map.put(TextFieldPage.class, NavTree.Item.page( map.put(TextFieldPage.class, NavTree.Item.page(
@ -269,15 +284,12 @@ public class MainModel {
map.put(ToggleSwitchPage.class, NavTree.Item.page(ToggleSwitchPage.NAME, ToggleSwitchPage.class)); map.put(ToggleSwitchPage.class, NavTree.Item.page(ToggleSwitchPage.NAME, ToggleSwitchPage.class));
map.put(ToolBarPage.class, NavTree.Item.page(ToolBarPage.NAME, ToolBarPage.class)); map.put(ToolBarPage.class, NavTree.Item.page(ToolBarPage.NAME, ToolBarPage.class));
map.put(TooltipPage.class, NavTree.Item.page(TooltipPage.NAME, TooltipPage.class)); map.put(TooltipPage.class, NavTree.Item.page(TooltipPage.NAME, TooltipPage.class));
map.put(TreePage.class, NavTree.Item.page(TreePage.NAME, TreePage.class)); map.put(TreeTableViewPage.class, NavTree.Item.page(TreeTableViewPage.NAME, TreeTableViewPage.class));
map.put(TreeTablePage.class, NavTree.Item.page(TreeTablePage.NAME, TreeTablePage.class)); map.put(TreeViewPage.class, NavTree.Item.page(TreeViewPage.NAME, TreeViewPage.class));
// showcases
map.put(FileManagerPage.class, NavTree.Item.page(FileManagerPage.NAME, FileManagerPage.class)); map.put(FileManagerPage.class, NavTree.Item.page(FileManagerPage.NAME, FileManagerPage.class));
map.put(MusicPlayerPage.class, NavTree.Item.page(MusicPlayerPage.NAME, MusicPlayerPage.class)); map.put(MusicPlayerPage.class, NavTree.Item.page(MusicPlayerPage.NAME, MusicPlayerPage.class));
map.put(WidgetCollectionPage.class, NavTree.Item.page(
WidgetCollectionPage.NAME,
WidgetCollectionPage.class, "Card", "Message", "Stepper", "Tag")
);
return map; return map;
} }

@ -65,7 +65,7 @@ public class NavTree extends TreeView<Nav> {
root.getChildren().setAll(titleLabel, new Spacer(), arrowIcon); root.getChildren().setAll(titleLabel, new Spacer(), arrowIcon);
root.setCursor(Cursor.HAND); root.setCursor(Cursor.HAND);
root.getStyleClass().add("container"); root.getStyleClass().add("container");
root.setMaxWidth(MainLayer.SIDEBAR_WIDTH - 10); root.setMaxWidth(ApplicationWindow.SIDEBAR_WIDTH - 10);
root.setOnMouseClicked(e -> { root.setOnMouseClicked(e -> {
if (!(getTreeItem() instanceof Item item)) { if (!(getTreeItem() instanceof Item item)) {

@ -8,7 +8,6 @@ import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.OverlayDialog; import atlantafx.sampler.page.OverlayDialog;
import java.util.function.Consumer; import java.util.function.Consumer;
import javafx.event.EventType;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;

@ -4,29 +4,22 @@ package atlantafx.sampler.page;
import static atlantafx.sampler.util.Containers.setScrollConstraints; import static atlantafx.sampler.util.Containers.setScrollConstraints;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED; import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.NEVER;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.layout.Overlay; import atlantafx.sampler.layout.Overlay;
import java.util.Arrays; import javafx.geometry.Pos;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import net.datafaker.Faker; import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public abstract class AbstractPage extends BorderPane implements Page { public abstract class AbstractPage extends StackPane implements Page {
protected static final Faker FAKER = new Faker(); protected final VBox userContent = new VBox();
protected static final Random RANDOM = new Random();
protected final StackPane userContent = new StackPane();
protected Overlay overlay; protected Overlay overlay;
protected boolean isRendered = false; protected boolean isRendered = false;
@ -40,11 +33,16 @@ public abstract class AbstractPage extends BorderPane implements Page {
} }
protected void createPageLayout() { protected void createPageLayout() {
var scrollPane = new ScrollPane(userContent); var userContentArea = new StackPane(userContent);
setScrollConstraints(scrollPane, AS_NEEDED, true, AS_NEEDED, true); userContentArea.setAlignment(Pos.TOP_CENTER);
scrollPane.setMaxHeight(10_000); userContent.setMinWidth(Math.min(Page.MAX_WIDTH, 800));
userContent.setMaxWidth(Math.min(Page.MAX_WIDTH, 800));
setCenter(scrollPane); var scrollPane = new ScrollPane(userContentArea);
setScrollConstraints(scrollPane, AS_NEEDED, true, NEVER, true);
scrollPane.setMaxHeight(20_000);
getChildren().setAll(scrollPane);
} }
protected void setUserContent(Node content) { protected void setUserContent(Node content) {
@ -87,23 +85,20 @@ public abstract class AbstractPage extends BorderPane implements Page {
this.overlay = lookupOverlay(); this.overlay = lookupOverlay();
} }
protected void addNode(Node node) {
userContent.getChildren().add(node);
}
protected void addPlainText(String text) {
userContent.getChildren().add(new TextFlow(new Text(text)));
}
protected void addFormattedText(String text) {
userContent.getChildren().add(BBCodeParser.createFormattedText(text));
}
protected Overlay lookupOverlay() { protected Overlay lookupOverlay() {
return getScene() != null && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay ov ? ov : null; return getScene() != null
} && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay ov ? ov : null;
///////////////////////////////////////////////////////////////////////////
protected HBox expandingHBox(Node... nodes) {
var box = new HBox(PAGE_HGAP, nodes);
Arrays.stream(nodes).forEach(n -> HBox.setHgrow(n, Priority.ALWAYS));
return box;
}
protected <T> List<T> generate(Supplier<T> supplier, int count) {
return Stream.generate(supplier).limit(count).toList();
}
protected Feather randomIcon() {
return Feather.values()[RANDOM.nextInt(Feather.values().length)];
} }
} }

@ -0,0 +1,70 @@
package atlantafx.sampler.page;
import atlantafx.base.util.BBCodeParser;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.scene.text.TextFlow;
// This code is adapted from RichTextFX JavaKeywordsDemo:
// https://github.com/FXMisc/RichTextFX/blob/master/richtextfx-demos
public class BBSyntaxHighlighter {
private static final String[] KEYWORDS = new String[] {
"abstract", "assert", "boolean", "break", "byte",
"case", "catch", "char", "class", "const",
"continue", "default", "do", "double", "else",
"enum", "extends", "final", "finally", "float",
"for", "if", "implements", "import",
"instanceof", "int", "interface", "long", "native",
"new", "package", "private", "protected", "public",
"return", "short", "static", "super",
"switch", "synchronized", "this", "throw", "throws",
"transient", "try", "void", "volatile", "while",
"var", "record", "with", "yield", "sealed", "non-sealed"
};
private static final String KEYWORD_PATTERN = "\\b(" + String.join("|", KEYWORDS) + ")\\b";
private static final String PAREN_PATTERN = "\\(|\\)";
private static final String STRING_PATTERN = "\"([^\"\\\\]|\\\\.)*\"";
private static final String COMMENT_PATTERN = "//[^\n]*" + "|" + "/\\*(.|\\R)*?\\*/"
+ "|" + "/\\*[^\\v]*" + "|" + "^\\h*\\*([^\\v]*|/)";
private static final Pattern PATTERN = Pattern.compile(
"(?<KEYWORD>" + KEYWORD_PATTERN + ")"
+ "|(?<STRING>" + STRING_PATTERN + ")"
+ "|(?<PAREN>" + PAREN_PATTERN + ")"
+ "|(?<COMMENT>" + COMMENT_PATTERN + ")"
);
// Enclose keywords to BBCode tags.
public static String format(String text) {
Matcher m = PATTERN.matcher(text);
StringBuilder sb = new StringBuilder();
int lastKwEnd = 0;
while (m.find()) {
String styleClass =
m.group("KEYWORD") != null ? "keyword" :
m.group("PAREN") != null ? "paren" :
m.group("STRING") != null ? "string" :
m.group("COMMENT") != null ? "comment" :
null;
sb.append(text, lastKwEnd, m.start())
.append("[span='")
.append(Objects.requireNonNullElse(styleClass, "absent"))
.append("']")
.append(text, m.start(), m.end())
.append("[/span]");
lastKwEnd = m.end();
}
sb.append(text, lastKwEnd, text.length());
return sb.toString();
}
public static TextFlow highlight(String text) {
return BBCodeParser.createFormattedText(format(text));
}
}

@ -0,0 +1,117 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import java.util.Objects;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
// This widget emulates TabPane behavior, because TabPane itself doesn't work as it should:
// https://bugs.openjdk.org/browse/JDK-8145490
public class ExampleBox extends VBox {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final ObjectProperty<Label> selectedTab = new SimpleObjectProperty<>();
private final ToggleSwitch stateToggle;
public ExampleBox(Node preview, Snippet snippet) {
this(preview, snippet, null);
}
public ExampleBox(Node preview, Snippet snippet, @Nullable Node description) {
super();
Objects.requireNonNull(preview, "preview");
Objects.requireNonNull(snippet, "snippet");
var previewTab = createTabLabel("Preview");
var codeTab = createTabLabel("Code");
var copyBtn = new Button("", new FontIcon(Material2OutlinedAL.CODE));
copyBtn.getStyleClass().addAll(Styles.BUTTON_CIRCLE, Styles.FLAT, Styles.SMALL);
copyBtn.setTooltip(new Tooltip("Copy source code"));
stateToggle = new ToggleSwitch();
HBox.setMargin(stateToggle, new Insets(0, 0, 0, 10));
var tabs = new HBox(
previewTab, codeTab,
new Spacer(), copyBtn, stateToggle
);
tabs.getStyleClass().add("tabs");
tabs.setAlignment(Pos.CENTER_LEFT);
var content = new VBox();
getStyleClass().add("example-box");
if (description != null) {
getChildren().add(description);
}
getChildren().addAll(tabs, content);
// == INIT ==
stateToggle.selectedProperty().addListener((obs, old, val) -> {
if (selectedTab.get() == previewTab) {
content.getChildren().forEach(c -> c.setDisable(val));
}
});
copyBtn.setOnAction(e -> {
var cc = new ClipboardContent();
cc.putString(snippet.getSourceCode());
Clipboard.getSystemClipboard().setContent(cc);
});
selectedTab.addListener((obs, old, val) -> {
if (val == codeTab) {
stateToggle.setDisable(true);
content.getChildren().setAll(snippet.render());
} else {
stateToggle.setDisable(false);
content.getChildren().setAll(preview);
}
if (old != null) {
old.pseudoClassStateChanged(SELECTED, false);
}
if (val != null) {
val.pseudoClassStateChanged(SELECTED, true);
}
});
selectedTab.set(previewTab);
}
public void setAllowDisable(boolean allow) {
stateToggle.setDisable(!allow);
stateToggle.setVisible(allow);
stateToggle.setManaged(allow);
}
private Label createTabLabel(String title) {
var label = new Label(title);
label.setOnMouseClicked(e -> selectedTab.set(label));
label.setPrefWidth(120);
label.setAlignment(Pos.CENTER);
return label;
}
}

@ -0,0 +1,277 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import static atlantafx.sampler.util.Containers.setScrollConstraints;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.NEVER;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.layout.Overlay;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public abstract class OutlinePage extends StackPane implements Page {
protected static final int OUTLINE_WIDTH = 200;
protected final ScrollPane scrollPane = new ScrollPane();
protected final VBox userContent = new VBox();
protected final Outline outline = new Outline(createOutlineHandler());
protected Overlay overlay;
protected boolean isRendered = false;
protected OutlinePage() {
super();
userContent.getStyleClass().add("user-content");
getStyleClass().add("outline-page");
createPageLayout();
}
protected void createPageLayout() {
var userContentArea = new StackPane(userContent);
StackPane.setMargin(userContent, new Insets(0, OUTLINE_WIDTH, 0, 0));
userContent.setMinWidth(Page.MAX_WIDTH - OUTLINE_WIDTH - 100);
userContent.setMaxWidth(Page.MAX_WIDTH - OUTLINE_WIDTH - 100);
scrollPane.setContent(userContentArea);
setScrollConstraints(scrollPane, AS_NEEDED, true, NEVER, true);
scrollPane.setMaxHeight(20_000);
// scroll spy
scrollPane.vvalueProperty().addListener((obs, old, val) ->
// we need a little gap between changing vValue and fetching header bounds
Platform.runLater(() -> outline.select(getFirstVisibleHeader()))
);
var pageBody = new StackPane();
pageBody.getChildren().setAll(scrollPane, outline);
pageBody.getStyleClass().add("body");
StackPane.setAlignment(outline, Pos.TOP_RIGHT);
StackPane.setMargin(outline, new Insets(50, 20, 0, 0));
setMinWidth(Page.MAX_WIDTH);
getChildren().setAll(pageBody);
}
protected Consumer<Heading> createOutlineHandler() {
return heading -> {
if (heading != Heading.TOP) {
Parent container = heading.anchor().getParent();
int indexInParent = container.getChildrenUnmodifiable().indexOf(heading.anchor());
Node target;
double targetY;
if (container.getChildrenUnmodifiable().size() > indexInParent + 1) {
// aims to the middle of the content node (the one that is next to header)
target = container.getChildrenUnmodifiable().get(indexInParent + 1);
var bounds = target.getBoundsInParent();
targetY = bounds.getMaxY() - (bounds.getHeight() / 2);
} else {
target = heading.anchor();
targetY = target.getBoundsInParent().getMaxY();
}
double height = scrollPane.getContent().getBoundsInLocal().getHeight();
scrollPane.setVvalue(targetY / height);
} else {
scrollPane.setVvalue(0);
}
};
}
private @Nullable String getFirstVisibleHeader() {
var scrollBounds = scrollPane.localToScene(scrollPane.getBoundsInParent());
Label lastHeading = null;
for (var node : userContent.getChildren()) {
if (!(node instanceof Label heading)) {
continue;
}
var headingBounds = heading.localToScene(heading.getBoundsInLocal());
if (outline.contains(heading.getText())) {
// viewport should fully contain heading bounds, not just a part of it
if (scrollBounds.contains(headingBounds)) {
return heading.getText();
} else {
lastHeading = heading;
}
}
}
return lastHeading != null ? lastHeading.getText() : null;
}
protected void addNode(Node node) {
userContent.getChildren().add(node);
}
protected void addPlainText(String text) {
userContent.getChildren().add(new TextFlow(new Text(text)));
}
protected void addFormattedText(String text) {
addFormattedText(text, false);
}
protected void addFormattedText(String text, boolean handleUrl) {
userContent.getChildren().add(createFormattedText(text, handleUrl));
}
protected void addSection(String title, Node content) {
var titleIcon = new FontIcon(Feather.HASH);
titleIcon.getStyleClass().add("icon-subtle");
var titleLabel = new Label(title);
titleLabel.getStyleClass().add(Styles.TITLE_3);
titleLabel.setGraphic(titleIcon);
titleLabel.setGraphicTextGap(10);
titleLabel.setPadding(new Insets(20, 0, 0, 0));
userContent.getChildren().addAll(titleLabel, content);
outline.add(new Heading(title, titleLabel));
}
@Override
public Pane getView() {
return this;
}
@Override
public boolean canDisplaySourceCode() {
return true;
}
@Override
public boolean canChangeThemeSettings() {
return true;
}
@Override
public void reset() {
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isRendered) {
return;
}
isRendered = true;
onRendered();
}
// Some properties can only be obtained after node placed
// to the scene graph and here is the place do this.
protected void onRendered() {
this.overlay = lookupOverlay();
}
protected Overlay lookupOverlay() {
var scene = getScene();
return scene != null && scene.lookup("." + Overlay.STYLE_CLASS) instanceof Overlay o ? o : null;
}
///////////////////////////////////////////////////////////////////////////
public record Heading(String title, Node anchor) {
private static final Heading TOP = new Heading("Top", new Text());
public Heading {
Objects.requireNonNull(title, "title");
Objects.requireNonNull(anchor, "anchor");
}
}
public static class Outline extends VBox {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final Set<String> toc = new LinkedHashSet<>();
private final Consumer<Heading> clickHandler;
private Label selected;
public Outline(Consumer<Heading> clickHandler) {
super();
this.clickHandler = Objects.requireNonNull(clickHandler, "clickHandler");
// outline has two items by default at the bottom
getChildren().add(new Separator());
getChildren().add(createEntry(Heading.TOP));
setPickOnBounds(false); // do not consume scroll events on transparent pixels
getStyleClass().add("outline");
setMinWidth(OUTLINE_WIDTH);
setMaxWidth(OUTLINE_WIDTH);
}
public void add(Heading heading) {
var label = createEntry(heading);
if (getChildren().size() == 2) { // top entry
label.pseudoClassStateChanged(SELECTED, true);
this.selected = label;
}
Objects.requireNonNull(heading, "heading");
getChildren().add(getChildren().size() - 2, label);
toc.add(heading.title());
}
public boolean contains(String title) {
return title != null && toc.contains(title);
}
public void select(String title) {
if (selected != null) {
selected.pseudoClassStateChanged(SELECTED, false);
}
for (var node : getChildren()) {
if (node instanceof Label label && Objects.equals(label.getText(), title)) {
label.pseudoClassStateChanged(SELECTED, true);
selected = label;
break; // handle multiple entries share the same title
}
}
}
private Label createEntry(Heading heading) {
var label = new Label(heading.title());
label.setWrapText(true);
label.setMaxWidth(Double.MAX_VALUE);
label.setOnMouseClicked(e -> {
if (e.getClickCount() == 1 && e.getButton() == MouseButton.PRIMARY) {
clickHandler.accept(heading);
}
});
return label;
}
}
}

@ -2,10 +2,33 @@
package atlantafx.sampler.page; package atlantafx.sampler.page;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.event.BrowseEvent;
import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.layout.ApplicationWindow;
import java.net.URI;
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.Parent; import javafx.scene.Parent;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import net.datafaker.Faker;
import org.kordamp.ikonli.feather.Feather;
public interface Page { public interface Page {
int MAX_WIDTH = ApplicationWindow.MIN_WIDTH - ApplicationWindow.SIDEBAR_WIDTH;
int HGAP_20 = 20;
int HGAP_30 = 30;
int VGAP_10 = 10;
int VGAP_20 = 20;
Faker FAKER = new Faker();
Random RANDOM = new Random();
int PAGE_HGAP = 30; int PAGE_HGAP = 30;
int PAGE_VGAP = 30; int PAGE_VGAP = 30;
@ -18,4 +41,35 @@ public interface Page {
boolean canChangeThemeSettings(); boolean canChangeThemeSettings();
void reset(); void reset();
default <T> List<T> generate(Supplier<T> supplier, int count) {
return Stream.generate(supplier).limit(count).toList();
}
default Feather randomIcon() {
return Feather.values()[RANDOM.nextInt(Feather.values().length)];
}
default Node createFormattedText(String text, boolean handleUrl) {
var node = BBCodeParser.createFormattedText(text);
if (handleUrl) {
node.addEventFilter(ActionEvent.ACTION, e -> {
if (e.getTarget() instanceof Hyperlink link && link.getUserData() != null) {
DefaultEventBus.getInstance().publish(
new BrowseEvent(URI.create((String) link.getUserData()))
);
}
e.consume();
});
}
return node;
}
default Label captionLabel(String text) {
var label = new Label(text);
label.setStyle("-fx-font-family:monospace");
return label;
}
} }

@ -1,80 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import java.util.Objects;
import java.util.Random;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import net.datafaker.Faker;
import org.kordamp.ikonli.feather.Feather;
public class SampleBlock extends VBox {
public static final int BLOCK_HGAP = 20;
public static final int BLOCK_VGAP = 10;
protected static final Faker FAKER = new Faker();
protected static final Random RANDOM = new Random();
protected final Label titleLabel;
protected final Node content; // can be either Pane or Control
protected TextFlow descriptionText;
public SampleBlock(String title, Node content) {
this(title, content, null);
}
public SampleBlock(String title, Node content, String description) {
titleLabel = new Label(Objects.requireNonNull(title));
Polygon polygon = new Polygon();
polygon.getPoints().addAll(0.0, 0.0, 20.0, 10.0, 20.0, 0.0);
polygon.getStyleClass().add("polygon");
var titleBox = new VBox(titleLabel, polygon);
titleBox.getStyleClass().add("title");
VBox.setMargin(titleBox, new Insets(-5, 0, 0, -40));
this.content = Objects.requireNonNull(content);
content.getStyleClass().add("content");
getChildren().setAll(titleBox, content);
if (description != null && !description.isBlank()) {
descriptionText = new TextFlow(new Text(description));
getChildren().add(descriptionText);
}
getStyleClass().add("sample-block");
}
public String getTitle() {
return titleLabel.getText();
}
public void setTitle(String text) {
titleLabel.setText(text);
}
public Node getContent() {
return content;
}
public void setFillHeight(boolean fillHeight) {
if (fillHeight) {
VBox.setVgrow(content, Priority.ALWAYS);
} else {
VBox.setVgrow(content, Priority.NEVER);
}
}
protected static Feather randomIcon() {
return Feather.values()[RANDOM.nextInt(Feather.values().length)];
}
}

@ -0,0 +1,59 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import java.io.IOException;
import java.util.Objects;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class Snippet {
private final Class<?> sourceClass;
private final int id;
private HBox container = null;
public Snippet(Class<?> sourceClass, int id) {
this.sourceClass = Objects.requireNonNull(sourceClass, "sourceClass");
this.id = id;
}
public Node render() {
var snippet = getSourceCode();
if (container == null && !snippet.isBlank()) {
var textFlow = new TextFlow(BBSyntaxHighlighter.highlight(snippet));
HBox.setHgrow(textFlow, Priority.ALWAYS);
container = new HBox(textFlow);
container.getStyleClass().add("snippet");
}
return Objects.requireNonNullElse(container, new TextFlow(new Text("Code snippet not found.")));
}
public String getSourceCode() {
var sourceFileName = sourceClass.getSimpleName() + ".java";
try (var stream = sourceClass.getResourceAsStream(sourceFileName)) {
Objects.requireNonNull(stream, "Missing source file '" + sourceFileName + "';");
var sourceCode = new String(stream.readAllBytes());
var startTag = "//snippet_" + id + ":start";
var endTag = "//snippet_" + id + ":end";
var start = sourceCode.indexOf(startTag);
var end = sourceCode.indexOf(endTag);
var snippet = "";
if (start >= 0 && end >= 0) {
snippet = sourceCode.substring(start + startTag.length(), end)
.stripIndent()
.trim();
}
return snippet;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@ -2,20 +2,20 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.function.Supplier;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Accordion; import javafx.scene.control.Accordion;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
@ -23,11 +23,12 @@ import javafx.scene.control.TitledPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
public class AccordionPage extends AbstractPage { public class AccordionPage extends OutlinePage {
public static final String NAME = "Accordion"; public static final String NAME = "Accordion";
@ -36,70 +37,130 @@ public class AccordionPage extends AbstractPage {
return NAME; return NAME;
} }
private final BooleanProperty expandedProperty = new SimpleBooleanProperty(true);
private final BooleanProperty animatedProperty = new SimpleBooleanProperty(true);
private final Accordion accordion;
public AccordionPage() { public AccordionPage() {
super(); super();
accordion = createPlayground(); addFormattedText("""
var sample = new SampleBlock( A user interface component that allows you to display a list of expandable \
"Playground", items and only one item can be open at a time. Each item in the [i]Accordion[/i] \
new VBox(SampleBlock.BLOCK_VGAP, createControls(), accordion) is made up of two parts, the header, and the content. The header is typically \
a text or graphic and the content can be any valid JavaFX node. When a user \
clicks on a header of an item, it will expand or collapse its content."""
); );
sample.setFillHeight(true); addSection("Usage", usageExample());
setUserContent(sample); addSection("Dense", denseExample());
addSection("Alternative Icon", altIconExample());
addSection("Playground", playground());
} }
private HBox createControls() { private ExampleBox usageExample() {
var animatedToggle = new ToggleSwitch("Animated"); //snippet_1:start
animatedProperty.bind(animatedToggle.selectedProperty()); Supplier<Node> gen = () -> {
animatedToggle.setSelected(true); var text = FAKER.lorem().paragraph();
var textFlow = new TextFlow(new Text(text));
textFlow.setMinHeight(100);
VBox.setVgrow(textFlow, Priority.ALWAYS);
return new VBox(textFlow);
};
var expandedToggle = new ToggleSwitch("Always expanded"); var tp1 = new TitledPane("Item 1", gen.get());
expandedProperty.bind(expandedToggle.selectedProperty()); var tp2 = new TitledPane("Item 2", gen.get());
expandedToggle.setSelected(true); var tp3 = new TitledPane("Item 3", gen.get());
var accordion = new Accordion(tp1, tp2, tp3);
//snippet_1:end
var denseToggle = new ToggleSwitch("Dense"); var description = BBCodeParser.createFormattedText("""
denseToggle.selectedProperty().addListener( An [i]Accordion[/i] consists of a group of [i]TitlePanes[/i], \
(obs, old, val) -> accordion.getPanes().forEach(p -> toggleStyleClass(p, DENSE)) each of which can have its content expanded or collapsed."""
); );
var altIconToggle = new ToggleSwitch("Alt icon"); return new ExampleBox(accordion, new Snippet(getClass(), 1), description);
altIconToggle.selectedProperty().addListener(
(obs, old, val) -> accordion.getPanes().forEach(p -> toggleStyleClass(p, Tweaks.ALT_ICON))
);
var controls = new HBox(
BLOCK_HGAP,
animatedToggle,
expandedToggle,
denseToggle,
altIconToggle
);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(0, 0, 0, 2));
return controls;
} }
private Accordion createPlayground() { private ExampleBox denseExample() {
var textBlockContent = new Label(FAKER.chuckNorris().fact()); //snippet_2:start
var textBlock = new TitledPane("_Quote", textBlockContent); Supplier<Node> gen = () -> {
var text = FAKER.lorem().paragraph();
var textFlow = new TextFlow(new Text(text));
textFlow.setMinHeight(100);
VBox.setVgrow(textFlow, Priority.ALWAYS);
return new VBox(textFlow);
};
var tp1 = new TitledPane("Item 1", gen.get());
tp1.getStyleClass().add(Styles.DENSE);
var tp2 = new TitledPane("Item 2", gen.get());
tp2.getStyleClass().add(Styles.DENSE);
var tp3 = new TitledPane("Item 3", gen.get());
tp3.getStyleClass().add(Styles.DENSE);
var accordion = new Accordion(tp1, tp2, tp3);
//snippet_2:end
var description = BBCodeParser.createFormattedText("""
If you need more compact view there's [code]Styles.DENSE[/code] for that."""
);
return new ExampleBox(accordion, new Snippet(getClass(), 2), description);
}
private ExampleBox altIconExample() {
//snippet_3:start
Supplier<Node> gen = () -> {
var text = FAKER.lorem().paragraph();
var textFlow = new TextFlow(new Text(text));
textFlow.setMinHeight(100);
VBox.setVgrow(textFlow, Priority.ALWAYS);
return new VBox(textFlow);
};
var tp1 = new TitledPane("Item 1", gen.get());
tp1.getStyleClass().add(Tweaks.ALT_ICON);
var tp2 = new TitledPane("Item 2", gen.get());
tp2.getStyleClass().add(Tweaks.ALT_ICON);
var tp3 = new TitledPane("Item 3", gen.get());
tp3.getStyleClass().add(Tweaks.ALT_ICON);
var accordion = new Accordion(tp1, tp2, tp3);
//snippet_3:end
var description = BBCodeParser.createFormattedText("""
There's also additional tweak [code]Tweaks.ALT_ICON[/code] to change header \
arrow icon to the classic style."""
);
return new ExampleBox(accordion, new Snippet(getClass(), 3), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private VBox playground() {
final var expandedProperty = new SimpleBooleanProperty(true);
final var animatedProperty = new SimpleBooleanProperty(true);
// == ACCORDION ==
var labelBlock = new TitledPane("_Quote", new Label(FAKER.chuckNorris().fact()));
labelBlock.setMnemonicParsing(true);
labelBlock.animatedProperty().bind(animatedProperty);
var textFlow = new TextFlow(new Text(
String.join("\n\n", FAKER.lorem().paragraphs(10)))
);
textFlow.setPadding(new Insets(0, 10, 0, 0));
var textScroll = new ScrollPane(textFlow);
textScroll.setMinHeight(200);
textScroll.setFitToWidth(true);
var textBlock = new TitledPane("_Scrollable Text", textScroll);
textBlock.setMnemonicParsing(true); textBlock.setMnemonicParsing(true);
textBlock.animatedProperty().bind(animatedProperty); textBlock.animatedProperty().bind(animatedProperty);
var textFlow = new TextFlow(new Text(String.join("\n\n", FAKER.lorem().paragraphs(10))));
textFlow.setPadding(new Insets(0, 10, 0, 0));
var scrollTextBlockContent = new ScrollPane(textFlow);
scrollTextBlockContent.setMinHeight(200);
scrollTextBlockContent.setFitToWidth(true);
var scrollableTextBlock = new TitledPane("_Scrollable Text", scrollTextBlockContent);
scrollableTextBlock.setMnemonicParsing(true);
scrollableTextBlock.animatedProperty().bind(animatedProperty);
var disabledBlock = new TitledPane("Disabled Block", null); var disabledBlock = new TitledPane("Disabled Block", null);
disabledBlock.setDisable(true); disabledBlock.setDisable(true);
@ -110,14 +171,7 @@ public class AccordionPage extends AbstractPage {
imageBlock.animatedProperty().bind(animatedProperty); imageBlock.animatedProperty().bind(animatedProperty);
imageBlock.setMnemonicParsing(true); imageBlock.setMnemonicParsing(true);
// ~ var accordion = new Accordion(labelBlock, textBlock, disabledBlock, imageBlock);
var accordion = new Accordion(
textBlock,
scrollableTextBlock,
disabledBlock,
imageBlock
);
// prevents accordion from being completely collapsed // prevents accordion from being completely collapsed
accordion.expandedPaneProperty().addListener((obs, old, val) -> { accordion.expandedPaneProperty().addListener((obs, old, val) -> {
@ -128,6 +182,35 @@ public class AccordionPage extends AbstractPage {
}); });
accordion.setExpandedPane(accordion.getPanes().get(1)); accordion.setExpandedPane(accordion.getPanes().get(1));
return accordion; // == TOGGLES ==
var animatedToggle = new ToggleSwitch("Animated");
animatedProperty.bind(animatedToggle.selectedProperty());
animatedToggle.setSelected(true);
var expandedToggle = new ToggleSwitch("Always expanded");
expandedProperty.bind(expandedToggle.selectedProperty());
expandedToggle.setSelected(true);
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, val) -> accordion.getPanes().forEach(p -> Styles.toggleStyleClass(p, Styles.DENSE))
);
var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener(
(obs, old, val) -> accordion.getPanes().forEach(p -> Styles.toggleStyleClass(p, Tweaks.ALT_ICON))
);
var controls = new HBox(HGAP_20, animatedToggle, expandedToggle, denseToggle, altIconToggle);
controls.setAlignment(Pos.CENTER);
controls.setPadding(new Insets(0, 0, 0, 2));
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]Accordion[/i] features \
and also serves as an object for monkey testing."""
);
return new VBox(VGAP_10, description, accordion, controls);
} }
} }

@ -1,115 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Breadcrumbs;
import atlantafx.base.controls.Breadcrumbs.BreadCrumbItem;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class BreadcrumbsPage extends AbstractPage {
public static final String NAME = "Breadcrumbs";
private static final int CRUMB_COUNT = 5;
@Override
public String getName() {
return NAME;
}
public BreadcrumbsPage() {
super();
setUserContent(new VBox(
Page.PAGE_VGAP,
basicSample(),
customCrumbSample(),
customDividerSample()
));
}
private SampleBlock basicSample() {
return new SampleBlock("Basic", createBreadcrumbs(null, null));
}
private SampleBlock customCrumbSample() {
Callback<BreadCrumbItem<String>, ButtonBase> crumbFactory = crumb -> {
var btn = new Button(crumb.getValue(), new FontIcon(randomIcon()));
btn.getStyleClass().add(Styles.FLAT);
btn.setFocusTraversable(false);
return btn;
};
return new SampleBlock("Flat Button", createBreadcrumbs(crumbFactory, null));
}
private SampleBlock customDividerSample() {
Callback<BreadCrumbItem<String>, ? extends Node> dividerFactory = item -> {
if (item == null) {
return new Label("", new FontIcon(Material2AL.HOME));
}
return !item.isLast() ? new Label("", new FontIcon(Material2AL.CHEVRON_RIGHT)) : null;
};
return new SampleBlock("Custom Divider", createBreadcrumbs(null, dividerFactory));
}
private HBox createBreadcrumbs(Callback<BreadCrumbItem<String>, ButtonBase> crumbFactory,
Callback<BreadCrumbItem<String>, ? extends Node> dividerFactory) {
BreadCrumbItem<String> model = Breadcrumbs.buildTreeModel(
generate(() -> FAKER.science().element(), CRUMB_COUNT).toArray(String[]::new)
);
var nextBtn = new Button("Next");
nextBtn.getStyleClass().addAll(Styles.ACCENT);
var breadcrumbs = new Breadcrumbs<>(model);
breadcrumbs.setSelectedCrumb(getAncestor(model, CRUMB_COUNT / 2));
if (crumbFactory != null) {
breadcrumbs.setCrumbFactory(crumbFactory);
}
if (dividerFactory != null) {
breadcrumbs.setDividerFactory(dividerFactory);
}
nextBtn.setOnAction(e -> {
BreadCrumbItem<String> selected = breadcrumbs.getSelectedCrumb();
if (selected.getChildren().size() > 0) {
breadcrumbs.setSelectedCrumb((BreadCrumbItem<String>) selected.getChildren().get(0));
}
});
breadcrumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
if (val != null) {
nextBtn.setDisable(val.getChildren().isEmpty());
}
});
var box = new HBox(40, nextBtn, breadcrumbs);
box.setAlignment(Pos.CENTER_LEFT);
return box;
}
private <T> BreadCrumbItem<T> getAncestor(BreadCrumbItem<T> node, int height) {
var counter = height;
var current = node;
while (counter > 0 && current.getParent() != null) {
current = (BreadCrumbItem<T>) current.getParent();
counter--;
}
return current;
}
}

@ -2,33 +2,23 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.BUTTON_CIRCLE; import atlantafx.base.util.BBCodeParser;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import atlantafx.sampler.page.ExampleBox;
import static atlantafx.base.theme.Styles.BUTTON_OUTLINED; import atlantafx.sampler.page.OutlinePage;
import static atlantafx.base.theme.Styles.DANGER; import atlantafx.sampler.page.Snippet;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.base.theme.Styles.LARGE;
import static atlantafx.base.theme.Styles.ROUNDED;
import static atlantafx.base.theme.Styles.SMALL;
import static atlantafx.base.theme.Styles.SUCCESS;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment; import atlantafx.sampler.theme.CSSFragment;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle; import javafx.scene.shape.Circle;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL; import org.kordamp.ikonli.material2.Material2AL;
public class ButtonPage extends AbstractPage { public class ButtonPage extends OutlinePage {
public static final String NAME = "Button"; public static final String NAME = "Button";
@ -39,223 +29,382 @@ public class ButtonPage extends AbstractPage {
public ButtonPage() { public ButtonPage() {
super(); super();
createView();
addFormattedText("""
A simple button control. The button control can contain text and/or a graphic.
A button control has three different modes:
[ul]
[li]Normal: A normal push button.[/li]
[li]Default: A default button is the button that receives a keyboard [code]VK_ENTER[/code] press,
if no other node in the scene consumes it.[/li]
[li]Cancel: A cancel button is the button that receives a keyboard [code]VK_ESC[/code] press,
if no other node in the scene consumes it.[/li][/ul]"""
);
addSection("Usage", usageExample());
addSection("Colored", coloredButtonExample());
addSection("Icon Button", iconButtonExample());
addSection("Circular", circularButtonExample());
addSection("Outlined", outlinedButtonExample());
addSection("Rounded", roundedButtonExample());
addSection("Button Size", buttonSizeExample());
addSection("Custom Styles", customColorExample());
} }
private void createView() { private ExampleBox usageExample() {
var grid = new GridPane(); //snippet_1:start
grid.setHgap(Page.PAGE_HGAP); var normalBtn = new Button("_Normal");
grid.setVgap(Page.PAGE_VGAP); normalBtn.setMnemonicParsing(true);
grid.add(basicSample(), 0, 0);
grid.add(iconButtonSample(), 1, 0);
grid.add(coloredSample(), 0, 1);
grid.add(circularSample(), 1, 1);
grid.add(outlinedSample(), 0, 2);
grid.add(sizeSample(), 1, 2);
grid.add(roundedSample(), 0, 3);
grid.add(customColorSample(), 1, 3);
grid.add(disabledSample(), 0, 4);
setUserContent(grid);
}
private SampleBlock basicSample() {
var basicBtn = new Button("_Basic");
basicBtn.setMnemonicParsing(true);
var defaultBtn = new Button("_Default"); var defaultBtn = new Button("_Default");
defaultBtn.setDefaultButton(true); defaultBtn.setDefaultButton(true);
defaultBtn.setMnemonicParsing(true); defaultBtn.setMnemonicParsing(true);
var flatBtn = new Button("_Flat"); var outlinedBtn = new Button("Out_lined");
flatBtn.getStyleClass().add(FLAT); outlinedBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.ACCENT
);
outlinedBtn.setMnemonicParsing(true);
var content = new HBox(BLOCK_HGAP, basicBtn, defaultBtn, flatBtn); var flatBtn = new Button("_Flat");
return new SampleBlock("Basic", content); flatBtn.getStyleClass().add(Styles.FLAT);
//snippet_1:end
var box = new HBox(HGAP_20, normalBtn, defaultBtn, outlinedBtn, flatBtn);
var description = BBCodeParser.createFormattedText("""
The [i]Button[/i] comes with four CSS variants: normal (default), colored, \
outlined, and flat (or text). To change the appearance of the [i]Button[/i], \
you set the corresponding style classes that work as modifiers."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock coloredSample() { private ExampleBox coloredButtonExample() {
//snippet_2:start
var accentBtn = new Button("_Accent"); var accentBtn = new Button("_Accent");
accentBtn.getStyleClass().add(ACCENT); accentBtn.getStyleClass().add(Styles.ACCENT);
accentBtn.setMnemonicParsing(true); accentBtn.setMnemonicParsing(true);
var successBtn = new Button("_Success", new FontIcon(Feather.CHECK)); var successBtn = new Button(
successBtn.getStyleClass().add(SUCCESS); "_Success", new FontIcon(Feather.CHECK)
);
successBtn.getStyleClass().add(Styles.SUCCESS);
successBtn.setMnemonicParsing(true); successBtn.setMnemonicParsing(true);
var dangerBtn = new Button("Da_nger", new FontIcon(Feather.TRASH)); var dangerBtn = new Button(
dangerBtn.getStyleClass().add(DANGER); "Da_nger", new FontIcon(Feather.TRASH)
);
dangerBtn.getStyleClass().add(Styles.DANGER);
dangerBtn.setContentDisplay(ContentDisplay.RIGHT); dangerBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerBtn.setMnemonicParsing(true); dangerBtn.setMnemonicParsing(true);
var content = new HBox(BLOCK_HGAP, accentBtn, successBtn, dangerBtn); // ~
return new SampleBlock("Colored", content); var accentOutBtn = new Button("Accen_t");
accentOutBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.ACCENT
);
accentOutBtn.setMnemonicParsing(true);
var successOutBtn = new Button(
"S_uccess", new FontIcon(Feather.CHECK)
);
successOutBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.SUCCESS
);
successOutBtn.setMnemonicParsing(true);
var dangerOutBtn = new Button(
"Dan_ger", new FontIcon(Feather.TRASH)
);
dangerOutBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.DANGER
);
dangerOutBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerOutBtn.setMnemonicParsing(true);
// ~
var accentFlatBtn = new Button("Accen_t");
accentFlatBtn.getStyleClass().addAll(
Styles.FLAT, Styles.ACCENT
);
accentFlatBtn.setMnemonicParsing(true);
var successFlatBtn = new Button(
"S_uccess", new FontIcon(Feather.CHECK)
);
successFlatBtn.getStyleClass().addAll(
Styles.FLAT, Styles.SUCCESS
);
successFlatBtn.setMnemonicParsing(true);
var dangerFlatBtn = new Button(
"Dan_ger", new FontIcon(Feather.TRASH)
);
dangerFlatBtn.getStyleClass().addAll(
Styles.FLAT, Styles.DANGER
);
dangerFlatBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerFlatBtn.setMnemonicParsing(true);
//snippet_2:end
var box = new VBox(
VGAP_20,
new HBox(HGAP_20, accentBtn, successBtn, dangerBtn),
new HBox(HGAP_20, accentOutBtn, successOutBtn, dangerOutBtn),
new HBox(HGAP_20, accentFlatBtn, successFlatBtn, dangerFlatBtn)
);
var description = BBCodeParser.createFormattedText("""
You can change the [i]Button[/i] color simply by using predefined style class modifiers."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
private SampleBlock iconButtonSample() { private ExampleBox iconButtonExample() {
var basicBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL)); //snippet_3:start
basicBtn.getStyleClass().addAll(BUTTON_ICON); var normalBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL));
normalBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
var accentBtn = new Button("", new FontIcon(Feather.MENU)); var accentBtn = new Button("", new FontIcon(Feather.MENU));
accentBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT); accentBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.ACCENT
);
var successBtn = new Button("", new FontIcon(Feather.CHECK)); var successBtn = new Button("", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_ICON, SUCCESS); successBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.SUCCESS
);
var dangerBtn = new Button("", new FontIcon(Feather.TRASH)); var dangerBtn = new Button("", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_ICON, BUTTON_OUTLINED, DANGER); dangerBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.BUTTON_OUTLINED, Styles.DANGER
);
var flatAccentBtn = new Button("", new FontIcon(Feather.MIC)); var flatAccentBtn = new Button("", new FontIcon(Feather.MIC));
flatAccentBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, ACCENT); flatAccentBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.FLAT, Styles.ACCENT
);
var flatSuccessBtn = new Button("", new FontIcon(Feather.USER)); var flatSuccessBtn = new Button("", new FontIcon(Feather.USER));
flatSuccessBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, SUCCESS); flatSuccessBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.FLAT, Styles.SUCCESS
);
var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR)); var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR));
flatDangerBtn.getStyleClass().addAll(BUTTON_ICON, FLAT, DANGER); flatDangerBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.FLAT, Styles.DANGER
);
//snippet_3:end
var content = new HBox(BLOCK_HGAP, var box = new HBox(HGAP_20,
basicBtn, accentBtn, successBtn, dangerBtn, normalBtn, accentBtn, successBtn, dangerBtn,
flatAccentBtn, flatSuccessBtn, flatDangerBtn flatAccentBtn, flatSuccessBtn, flatDangerBtn
); );
return new SampleBlock("Icon", content); var description = BBCodeParser.createFormattedText("""
Icon buttons are present in two variants. The first one is just a \
normal [i]Button[/i] but with no text and the second one is a flat button - \
suitable for toolbars and similar controls."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
} }
private SampleBlock circularSample() { private ExampleBox circularButtonExample() {
var basicBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL)); //snippet_4:start
basicBtn.getStyleClass().addAll(BUTTON_CIRCLE); var normalBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL));
basicBtn.setShape(new Circle(50)); normalBtn.getStyleClass().addAll(Styles.BUTTON_CIRCLE);
normalBtn.setShape(new Circle(50));
var accentBtn = new Button("", new FontIcon(Feather.MENU)); var accentBtn = new Button("", new FontIcon(Feather.MENU));
accentBtn.getStyleClass().addAll(BUTTON_CIRCLE, ACCENT); accentBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.ACCENT
);
accentBtn.setShape(new Circle(50)); accentBtn.setShape(new Circle(50));
var successBtn = new Button("", new FontIcon(Feather.CHECK)); var successBtn = new Button("", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_CIRCLE, SUCCESS); successBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.SUCCESS
);
successBtn.setShape(new Circle(50)); successBtn.setShape(new Circle(50));
var dangerBtn = new Button("", new FontIcon(Feather.TRASH)); var dangerBtn = new Button("", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_CIRCLE, BUTTON_OUTLINED, DANGER); dangerBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.BUTTON_OUTLINED, Styles.DANGER
);
dangerBtn.setShape(new Circle(50)); dangerBtn.setShape(new Circle(50));
var flatAccentBtn = new Button("", new FontIcon(Feather.MIC)); var flatAccentBtn = new Button("", new FontIcon(Feather.MIC));
flatAccentBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, ACCENT); flatAccentBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.FLAT, Styles.ACCENT
);
flatAccentBtn.setShape(new Circle(50)); flatAccentBtn.setShape(new Circle(50));
var flatSuccessBtn = new Button("", new FontIcon(Feather.USER)); var flatSuccessBtn = new Button("", new FontIcon(Feather.USER));
flatSuccessBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, SUCCESS); flatSuccessBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.FLAT, Styles.SUCCESS
);
flatSuccessBtn.setShape(new Circle(50)); flatSuccessBtn.setShape(new Circle(50));
var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR)); var flatDangerBtn = new Button("", new FontIcon(Feather.CROSSHAIR));
flatDangerBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT, DANGER); flatDangerBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.FLAT, Styles.DANGER
);
flatDangerBtn.setShape(new Circle(50)); flatDangerBtn.setShape(new Circle(50));
//snippet_4:end
var content = new HBox(BLOCK_HGAP, var box = new HBox(HGAP_20,
basicBtn, accentBtn, successBtn, dangerBtn, normalBtn, accentBtn, successBtn, dangerBtn,
flatAccentBtn, flatSuccessBtn, flatDangerBtn flatAccentBtn, flatSuccessBtn, flatDangerBtn
); );
return new SampleBlock("Circular", content);
var description = BBCodeParser.createFormattedText("""
You can also apply the [code]setShape()[/code] method to make the \
[i]Button[/i] look circular.""");
return new ExampleBox(box, new Snippet(getClass(), 4), description);
} }
private SampleBlock outlinedSample() { private ExampleBox outlinedButtonExample() {
var accentBtn = new Button("Accen_t"); //snippet_5:start
accentBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT); var accentOutBtn = new Button("Accen_t");
accentBtn.setMnemonicParsing(true); accentOutBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.ACCENT
);
accentOutBtn.setMnemonicParsing(true);
var successBtn = new Button("S_uccess", new FontIcon(Feather.CHECK)); var successOutBtn = new Button("S_uccess", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS); successOutBtn.getStyleClass().addAll(
successBtn.setMnemonicParsing(true); Styles.BUTTON_OUTLINED, Styles.SUCCESS
);
successOutBtn.setMnemonicParsing(true);
var dangerBtn = new Button("Dan_ger", new FontIcon(Feather.TRASH)); var dangerOutBtn = new Button("Dan_ger", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER); dangerOutBtn.getStyleClass().addAll(
dangerBtn.setContentDisplay(ContentDisplay.RIGHT); Styles.BUTTON_OUTLINED, Styles.DANGER
dangerBtn.setMnemonicParsing(true); );
dangerOutBtn.setContentDisplay(ContentDisplay.RIGHT);
dangerOutBtn.setMnemonicParsing(true);
//snippet_5:end
var content = new HBox(BLOCK_HGAP, accentBtn, successBtn, dangerBtn); var box = new HBox(HGAP_20, accentOutBtn, successOutBtn, dangerOutBtn);
var description = BBCodeParser.createFormattedText("""
Outlined buttons are medium-emphasis buttons. They contain actions that are \
important but aren't the primary action in an app."""
);
return new SampleBlock("Outlined", content); return new ExampleBox(box, new Snippet(getClass(), 5), description);
} }
private SampleBlock roundedSample() { private ExampleBox roundedButtonExample() {
var basicBtn = new Button("Basic"); //snippet_6:start
basicBtn.getStyleClass().addAll(SMALL, ROUNDED); var normalBtn = new Button("Normal");
normalBtn.getStyleClass().addAll(
Styles.SMALL, Styles.ROUNDED
);
var accentBtn = new Button("Accent"); var accentBtn = new Button("Accent");
accentBtn.getStyleClass().addAll(ROUNDED, ACCENT); accentBtn.getStyleClass().addAll(
Styles.ROUNDED, Styles.ACCENT
);
var successBtn = new Button("Success", new FontIcon(Feather.CHECK)); var successBtn = new Button("Success", new FontIcon(Feather.CHECK));
successBtn.getStyleClass().addAll(LARGE, ROUNDED, BUTTON_OUTLINED, SUCCESS); successBtn.getStyleClass().addAll(
Styles.LARGE, Styles.ROUNDED, Styles.BUTTON_OUTLINED, Styles.SUCCESS
);
//snippet_6:end
var content = new HBox(BLOCK_HGAP, basicBtn, accentBtn, successBtn); var box = new HBox(HGAP_20, normalBtn, accentBtn, successBtn);
content.setAlignment(Pos.CENTER_LEFT); box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Rounded", content); var description = BBCodeParser.createFormattedText("""
[i]Button[/i] corners can be rounded with the [code]Styles.ROUNDED[/code] \
style class modifier."""
);
return new ExampleBox(box, new Snippet(getClass(), 6), description);
} }
private SampleBlock sizeSample() { private ExampleBox buttonSizeExample() {
//snippet_7:start
var smallBtn = new Button("Small"); var smallBtn = new Button("Small");
smallBtn.getStyleClass().addAll(SMALL); smallBtn.getStyleClass().addAll(Styles.SMALL);
var normalBtn = new Button("Normal"); var normalBtn = new Button("Normal");
var largeBtn = new Button("Large"); var largeBtn = new Button("Large");
largeBtn.getStyleClass().addAll(LARGE); largeBtn.getStyleClass().addAll(Styles.LARGE);
//snippet_7:end
var content = new HBox(BLOCK_HGAP, smallBtn, normalBtn, largeBtn); var box = new HBox(HGAP_20, smallBtn, normalBtn, largeBtn);
content.setAlignment(Pos.CENTER_LEFT); box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Size", content); var description = BBCodeParser.createFormattedText("""
For larger or smaller buttons, use the [code]Styles.SMALL[/code] or \
[code]Styles.LARGE[/code] style classes, respectively."""
);
return new ExampleBox(box, new Snippet(getClass(), 7), description);
} }
private SampleBlock customColorSample() { private ExampleBox customColorExample() {
var btn = new Button("DO SOMETHING!"); var customStyle = """
btn.getStyleClass().addAll(SUCCESS, LARGE); -color-button-bg: linear-gradient(
btn.setStyle(""" to bottom right, -color-success-emphasis, darkblue
-color-button-bg: linear-gradient(to bottom right, -color-success-emphasis, darkblue); );
-color-button-bg-hover: -color-button-bg; -color-button-bg-hover: -color-button-bg;
-color-button-bg-focused: -color-button-bg; -color-button-bg-focused: -color-button-bg;
-color-button-bg-pressed: -color-button-bg; -color-button-bg-pressed: -color-button-bg;""";
""");
var iconBtn = new Button("", new FontIcon(Material2AL.FAVORITE)); String dataClass = """
iconBtn.getStyleClass().addAll("favorite-button", BUTTON_CIRCLE, FLAT, DANGER);
new CSSFragment("""
.favorite-button.button >.ikonli-font-icon { .favorite-button.button >.ikonli-font-icon {
-fx-fill: linear-gradient(to bottom right, pink, -color-danger-emphasis); -fx-fill: linear-gradient(
-fx-icon-color: linear-gradient(to bottom right, pink, -color-danger-emphasis); to bottom right, pink, -color-danger-emphasis
);
-fx-icon-color: linear-gradient(
to bottom right, pink, -color-danger-emphasis
);
-fx-font-size: 32px; -fx-font-size: 32px;
-fx-icon-size: 32px; -fx-icon-size: 32px;
} }""";
""").addTo(iconBtn);
var content = new HBox(BLOCK_HGAP, btn, iconBtn); //snippet_8:start
content.setAlignment(Pos.CENTER_LEFT); var btn = new Button("DO SOMETHING!");
btn.getStyleClass().addAll(Styles.SUCCESS, Styles.LARGE);
// -color-button-bg: linear-gradient(
// to bottom right, -color-success-emphasis, darkblue
// );
// -color-button-bg-hover: -color-button-bg;
// -color-button-bg-focused: -color-button-bg;
// -color-button-bg-pressed: -color-button-bg;
btn.setStyle(customStyle);
return new SampleBlock("Custom Color", content); var iconBtn = new Button("", new FontIcon(Material2AL.FAVORITE));
} iconBtn.getStyleClass().addAll(
"favorite-button",
Styles.BUTTON_CIRCLE, Styles.FLAT, Styles.DANGER
);
// .favorite-button.button >.ikonli-font-icon {
// -fx-fill: linear-gradient(
// to bottom right, pink, -color-danger-emphasis
// );
// -fx-icon-color: linear-gradient(
// to bottom right, pink, -color-danger-emphasis
// );
// -fx-font-size: 32px;
// -fx-icon-size: 32px;
// }
new CSSFragment(dataClass).addTo(iconBtn);
//snippet_8:end
private SampleBlock disabledSample() { var box = new HBox(HGAP_20, btn, iconBtn);
var basicBtn = new Button("Basic"); box.setAlignment(Pos.CENTER_LEFT);
basicBtn.setDisable(true);
var defaultBtn = new Button("Default"); var description = BBCodeParser.createFormattedText("""
defaultBtn.setDefaultButton(true); In addition to using the predefined [i]Button[/i] colors, you can add custom ones \
defaultBtn.setDisable(true); by manipulating the looked-up color variables."""
);
var flatBtn = new Button("Flat"); return new ExampleBox(box, new Snippet(getClass(), 8), description);
flatBtn.getStyleClass().addAll(FLAT);
flatBtn.setDisable(true);
var iconBtn = new Button("", new FontIcon(Feather.TAG));
iconBtn.getStyleClass().addAll(BUTTON_ICON);
iconBtn.setDisable(true);
var content = new HBox(BLOCK_HGAP, basicBtn, defaultBtn, flatBtn, iconBtn);
return new SampleBlock("Disabled", content);
} }
} }

@ -2,25 +2,20 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.base.controls.Spacer; import atlantafx.base.util.BBCodeParser;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import java.time.Month; import java.time.Month;
import java.time.format.TextStyle; import java.time.format.TextStyle;
import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.chart.AreaChart; import javafx.scene.chart.AreaChart;
import javafx.scene.chart.BarChart; import javafx.scene.chart.BarChart;
import javafx.scene.chart.BubbleChart; import javafx.scene.chart.BubbleChart;
import javafx.scene.chart.CategoryAxis; import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.Chart;
import javafx.scene.chart.LineChart; import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis; import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.PieChart; import javafx.scene.chart.PieChart;
@ -28,14 +23,8 @@ import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.StackedAreaChart; import javafx.scene.chart.StackedAreaChart;
import javafx.scene.chart.StackedBarChart; import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart; import javafx.scene.chart.XYChart;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
public class ChartPage extends AbstractPage { public class ChartPage extends OutlinePage {
public static final String NAME = "Chart"; public static final String NAME = "Chart";
@ -44,107 +33,27 @@ public class ChartPage extends AbstractPage {
return NAME; return NAME;
} }
private final BorderPane chartWrapper = new BorderPane();
private final ComboBox<Example> exampleSelect = new ComboBox<>();
public ChartPage() { public ChartPage() {
super(); super();
setUserContent(new VBox(
new SampleBlock("Playground", createPlayground())
));
}
private VBox createPlayground() { addPlainText("""
exampleSelect.setMaxWidth(Double.MAX_VALUE); JavaFX provides a set of chart components specifically designed \
exampleSelect.getItems().setAll(Example.values()); for data visualization. The charts include common types such as \
exampleSelect.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> { Bar, Line, Area, Pie, Scatter, and Bubble charts."""
if (val == null) {
return;
}
Chart newChart = createChart(val);
// copy existing properties to the new chart
findDisplayedChart().ifPresent(chart -> newChart.setDisable(chart.isDisable()));
chartWrapper.setCenter(newChart);
});
exampleSelect.setConverter(new StringConverter<>() {
@Override
public String toString(Example example) {
return example == null ? "" : example.getName();
}
@Override
public Example fromString(String s) {
return Example.find(s);
}
});
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedChart().ifPresent(ch -> {
if (val != null) {
ch.setDisable(val);
}
}));
var controls = new HBox(disableToggle);
controls.setAlignment(Pos.CENTER);
VBox playground = new VBox(SampleBlock.BLOCK_VGAP);
playground.getChildren().setAll(
new HBox(new Label("Select an example:"), new Spacer(), disableToggle),
exampleSelect,
chartWrapper
); );
addSection("Area Chart", areaChart());
return playground; addSection("Stacked Area Chart", stackedAreaChart());
} addSection("Bar Chart", barChart());
addSection("Stacked Bar Chart", stackedBarChart());
@Override addSection("Bubble Chart", bubbleChart());
protected void onRendered() { addSection("Line Chart", lineChart());
super.onRendered(); addSection("Pie Chart", pieChart());
exampleSelect.getSelectionModel().selectFirst(); addSection("Scatter Chart", scatterChart());
}
private Optional<Chart> findDisplayedChart() {
return chartWrapper.getChildren().size() > 0
? Optional.of((Chart) chartWrapper.getChildren().get(0))
: Optional.empty();
}
private Chart createChart(Example example) {
switch (example) {
case AREA_CHART -> {
return areaChart(false);
}
case BAR_CHART -> {
return barChart(false);
}
case BUBBLE_CHART -> {
return bubbleChart();
}
case LINE_CHART -> {
return lineChart();
}
case PIE_CHART -> {
return pieChart();
}
case SCATTER_CHART -> {
return scatterChart();
}
case STACKED_AREA_CHART -> {
return areaChart(true);
}
case STACKED_BAR_CHART -> {
return barChart(true);
}
default -> throw new IllegalArgumentException("Unexpected enum value: " + example);
}
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Chart areaChart(boolean stacked) { private ExampleBox areaChart() {
//snippet_1:start
var x = new NumberAxis(1, 31, 1); var x = new NumberAxis(1, 31, 1);
x.setLabel("Day"); x.setLabel("Day");
@ -163,15 +72,56 @@ public class ChartPage extends AbstractPage {
new XYChart.Data<>(i, FAKER.random().nextInt(15, 30)) new XYChart.Data<>(i, FAKER.random().nextInt(15, 30))
)); ));
var chart = stacked ? new StackedAreaChart<>(x, y) : new AreaChart<>(x, y); var chart = new AreaChart<>(x, y);
chart.setTitle("Temperature Monitoring"); chart.setTitle("Temperature Monitoring");
chart.setMinHeight(300);
chart.getData().addAll(april, may); chart.getData().addAll(april, may);
//snippet_1:end
return chart; var description = BBCodeParser.createFormattedText("""
Plots the area between the line that connects the data points and the axis. \
Good for comparing cumulated totals over time.""");
return new ExampleBox(chart, new Snippet(getClass(), 1), description);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Chart barChart(boolean stacked) { private ExampleBox stackedAreaChart() {
//snippet_2:start
var x = new NumberAxis(1, 31, 1);
x.setLabel("Day");
var y = new NumberAxis();
y.setLabel("Temperature");
var april = new XYChart.Series<Number, Number>();
april.setName("April");
IntStream.range(1, 30).forEach(i -> april.getData().add(
new XYChart.Data<>(i, FAKER.random().nextInt(15, 30))
));
var may = new XYChart.Series<Number, Number>();
may.setName("May");
IntStream.range(1, 30).forEach(i -> may.getData().add(
new XYChart.Data<>(i, FAKER.random().nextInt(15, 30))
));
var chart = new StackedAreaChart<>(x, y);
chart.setTitle("Temperature Monitoring");
chart.setMinHeight(300);
chart.getData().addAll(april, may);
//snippet_2:end
var description = BBCodeParser.createFormattedText("""
A variation of [i]AreaChart[/i] that displays trends of the contribution of each value."""
);
return new ExampleBox(chart, new Snippet(getClass(), 2), description);
}
@SuppressWarnings("unchecked")
private ExampleBox barChart() {
//snippet_3:start
final var rnd = FAKER.random(); final var rnd = FAKER.random();
final var countries = IntStream.range(0, 5).boxed() final var countries = IntStream.range(0, 5).boxed()
.map(i -> FAKER.country().countryCode3().toUpperCase()) .map(i -> FAKER.country().countryCode3().toUpperCase())
@ -201,15 +151,69 @@ public class ChartPage extends AbstractPage {
new XYChart.Data<>(countries.get(i), rnd.nextInt(10, 80)) new XYChart.Data<>(countries.get(i), rnd.nextInt(10, 80))
)); ));
var chart = stacked ? new StackedBarChart<>(x, y) : new BarChart<>(x, y); var chart = new BarChart<>(x, y);
chart.setTitle("Country Summary"); chart.setTitle("Country Summary");
chart.setMinHeight(300);
chart.getData().addAll(january, february, march); chart.getData().addAll(january, february, march);
//snippet_3:end
return chart; var description = BBCodeParser.createFormattedText("""
Plots rectangular bars with heights indicating data values they represent, \
and corresponding to the categories they belongs to. Used for displaying \
discontinuous or discrete data."""
);
return new ExampleBox(chart, new Snippet(getClass(), 3), description);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Chart bubbleChart() { private ExampleBox stackedBarChart() {
//snippet_4:start
final var rnd = FAKER.random();
final var countries = IntStream.range(0, 5).boxed()
.map(i -> FAKER.country().countryCode3().toUpperCase())
.toList();
var x = new CategoryAxis();
x.setLabel("Country");
var y = new NumberAxis(0, 80, 10);
y.setLabel("Value");
var january = new XYChart.Series<String, Number>();
january.setName("January");
IntStream.range(0, countries.size()).forEach(i -> january.getData().add(
new XYChart.Data<>(countries.get(i), rnd.nextInt(10, 80))
));
var february = new XYChart.Series<String, Number>();
february.setName("February");
IntStream.range(0, countries.size()).forEach(i -> february.getData().add(
new XYChart.Data<>(countries.get(i), rnd.nextInt(10, 80))
));
var march = new XYChart.Series<String, Number>();
march.setName("March");
IntStream.range(0, countries.size()).forEach(i -> march.getData().add(
new XYChart.Data<>(countries.get(i), rnd.nextInt(10, 80))
));
var chart = new StackedBarChart<>(x, y);
chart.setTitle("Country Summary");
chart.setMinHeight(300);
chart.getData().addAll(january, february, march);
//snippet_4:end
var description = BBCodeParser.createFormattedText("""
A variation of [i]BarChart[/i] that plots bars indicating data values for a category."""
);
return new ExampleBox(chart, new Snippet(getClass(), 4), description);
}
@SuppressWarnings("unchecked")
private ExampleBox bubbleChart() {
//snippet_5:start
final var rnd = FAKER.random(); final var rnd = FAKER.random();
var x = new NumberAxis(1, 53, 4); var x = new NumberAxis(1, 53, 4);
@ -221,24 +225,40 @@ public class ChartPage extends AbstractPage {
var series1 = new XYChart.Series<Number, Number>(); var series1 = new XYChart.Series<Number, Number>();
series1.setName(FAKER.commerce().productName()); series1.setName(FAKER.commerce().productName());
IntStream.range(1, 10).forEach(i -> series1.getData().add( IntStream.range(1, 10).forEach(i -> series1.getData().add(
new XYChart.Data<>(rnd.nextInt(1, 53), rnd.nextInt(10, 80), rnd.nextDouble(1, 10)) new XYChart.Data<>(
rnd.nextInt(1, 53),
rnd.nextInt(10, 80),
rnd.nextDouble(1, 10)
)
)); ));
var series2 = new XYChart.Series<Number, Number>(); var series2 = new XYChart.Series<Number, Number>();
series2.setName(FAKER.commerce().productName()); series2.setName(FAKER.commerce().productName());
IntStream.range(1, 10).forEach(i -> series2.getData().add( IntStream.range(1, 10).forEach(i -> series2.getData().add(
new XYChart.Data<>(rnd.nextInt(1, 53), rnd.nextInt(10, 80), rnd.nextDouble(1, 10)) new XYChart.Data<>(
rnd.nextInt(1, 53),
rnd.nextInt(10, 80),
rnd.nextDouble(1, 10)
)
)); ));
var chart = new BubbleChart<>(x, y); var chart = new BubbleChart<>(x, y);
chart.setTitle("Budget Monitoring"); chart.setTitle("Budget Monitoring");
chart.setMinHeight(300);
chart.getData().addAll(series1, series2); chart.getData().addAll(series1, series2);
//snippet_5:end
return chart; var description = BBCodeParser.createFormattedText("""
Plots bubbles for data points in a series. Each plotted entity depicts \
three parameters in a 2D chart and hence a unique chart type."""
);
return new ExampleBox(chart, new Snippet(getClass(), 5), description);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Chart lineChart() { private ExampleBox lineChart() {
//snippet_6:start
final var rnd = FAKER.random(); final var rnd = FAKER.random();
var x = new CategoryAxis(); var x = new CategoryAxis();
@ -250,23 +270,36 @@ public class ChartPage extends AbstractPage {
var series1 = new XYChart.Series<String, Number>(); var series1 = new XYChart.Series<String, Number>();
series1.setName(FAKER.stock().nsdqSymbol()); series1.setName(FAKER.stock().nsdqSymbol());
IntStream.range(1, 12).forEach(i -> series1.getData().add( IntStream.range(1, 12).forEach(i -> series1.getData().add(
new XYChart.Data<>(Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()), rnd.nextInt(10, 80)) new XYChart.Data<>(
Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()),
rnd.nextInt(10, 80)
)
)); ));
var series2 = new XYChart.Series<String, Number>(); var series2 = new XYChart.Series<String, Number>();
series2.setName(FAKER.stock().nsdqSymbol()); series2.setName(FAKER.stock().nsdqSymbol());
IntStream.range(1, 12).forEach(i -> series2.getData().add( IntStream.range(1, 12).forEach(i -> series2.getData().add(
new XYChart.Data<>(Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()), rnd.nextInt(10, 80)) new XYChart.Data<>(
Month.of(i).getDisplayName(TextStyle.SHORT, Locale.getDefault()),
rnd.nextInt(10, 80)
)
)); ));
var chart = new LineChart<>(x, y); var chart = new LineChart<>(x, y);
chart.setTitle("Stock Monitoring"); chart.setTitle("Stock Monitoring");
chart.setMinHeight(300);
chart.getData().addAll(series1, series2); chart.getData().addAll(series1, series2);
//snippet_6:end
return chart; var description = BBCodeParser.createFormattedText("""
Plots line between the data points in a series. Used usually to view data trends over time."""
);
return new ExampleBox(chart, new Snippet(getClass(), 6), description);
} }
private Chart pieChart() { private ExampleBox pieChart() {
//snippet_7:start
final var rnd = FAKER.random(); final var rnd = FAKER.random();
ObservableList<PieChart.Data> data = FXCollections.observableArrayList( ObservableList<PieChart.Data> data = FXCollections.observableArrayList(
@ -278,13 +311,21 @@ public class ChartPage extends AbstractPage {
); );
var chart = new PieChart(data); var chart = new PieChart(data);
chart.setMinHeight(300);
chart.setTitle("Imported Fruits"); chart.setTitle("Imported Fruits");
//snippet_7:end
return chart; var description = BBCodeParser.createFormattedText("""
Plots circular chart divided into segments with each segment representing a value \
as a proportion of the total. It looks like a Pie and hence the name."""
);
return new ExampleBox(chart, new Snippet(getClass(), 7), description);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private Chart scatterChart() { private ExampleBox scatterChart() {
//snippet_8:start
final var rnd = FAKER.random(); final var rnd = FAKER.random();
var x = new NumberAxis(0, 10, 1); var x = new NumberAxis(0, 10, 1);
@ -296,47 +337,32 @@ public class ChartPage extends AbstractPage {
var series1 = new XYChart.Series<Number, Number>(); var series1 = new XYChart.Series<Number, Number>();
series1.setName("Equities"); series1.setName("Equities");
IntStream.range(1, 10).forEach(i -> series1.getData().add( IntStream.range(1, 10).forEach(i -> series1.getData().add(
new XYChart.Data<>(rnd.nextDouble(0, 10), rnd.nextDouble(-100, 500)) new XYChart.Data<>(
rnd.nextDouble(0, 10),
rnd.nextDouble(-100, 500)
)
)); ));
var series2 = new XYChart.Series<Number, Number>(); var series2 = new XYChart.Series<Number, Number>();
series2.setName("Mutual funds"); series2.setName("Mutual funds");
IntStream.range(1, 10).forEach(i -> series2.getData().add( IntStream.range(1, 10).forEach(i -> series2.getData().add(
new XYChart.Data<>(rnd.nextDouble(0, 10), rnd.nextDouble(-100, 500)) new XYChart.Data<>(
rnd.nextDouble(0, 10),
rnd.nextDouble(-100, 500)
)
)); ));
var chart = new ScatterChart<>(x, y); var chart = new ScatterChart<>(x, y);
chart.setTitle("Investment Overview"); chart.setTitle("Investment Overview");
chart.setMinHeight(300);
chart.getData().addAll(series1, series2); chart.getData().addAll(series1, series2);
//snippet_8:end
return chart; var description = BBCodeParser.createFormattedText("""
} Plots symbols for the data points in a series. This type of chart is useful in viewing \
distribution of data and its correlation, if there is any clustering."""
);
private enum Example { return new ExampleBox(chart, new Snippet(getClass(), 8), description);
AREA_CHART("Area Chart"),
BAR_CHART("Bar Chart"),
BUBBLE_CHART("Bubble Chart"),
LINE_CHART("Line Chart"),
PIE_CHART("Pie Chart"),
SCATTER_CHART("Scatter Chart"),
STACKED_AREA_CHART("Stacked Area Chart"),
STACKED_BAR_CHART("Stacked Bar Chart");
private final String name;
Example(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Example find(String name) {
return Arrays.stream(Example.values())
.filter(example -> Objects.equals(example.getName(), name))
.findFirst()
.orElse(null);
}
} }
} }

@ -2,14 +2,15 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
public class CheckBoxPage extends AbstractPage { public class CheckBoxPage extends OutlinePage {
public static final String NAME = "CheckBox"; public static final String NAME = "CheckBox";
@ -23,46 +24,66 @@ public class CheckBoxPage extends AbstractPage {
public CheckBoxPage() { public CheckBoxPage() {
super(); super();
createView();
}
private void createView() { addFormattedText("""
setUserContent(new FlowPane( A tri-state selection control is typically skinned as a box \
PAGE_HGAP, PAGE_VGAP, with a checkmark or tick mark when checked."""
basicSample(),
indeterminateSample(),
disabledSample()
));
}
private SampleBlock basicSample() {
basicCheck = new CheckBox("_Check Me");
basicCheck.setMnemonicParsing(true);
return new SampleBlock("Basic", basicCheck);
}
private SampleBlock indeterminateSample() {
indeterminateCheck = new CheckBox("C_heck Me");
indeterminateCheck.setAllowIndeterminate(true);
indeterminateCheck.setIndeterminate(true);
indeterminateCheck.setMnemonicParsing(true);
return new SampleBlock("Indeterminate", indeterminateCheck);
}
private SampleBlock disabledSample() {
var basicCheck = new CheckBox("Check Me");
basicCheck.setSelected(true);
basicCheck.setDisable(true);
var indeterminateCheck = new CheckBox("Check Me");
indeterminateCheck.setAllowIndeterminate(true);
indeterminateCheck.setIndeterminate(true);
indeterminateCheck.setDisable(true);
return new SampleBlock(
"Disabled",
new HBox(SampleBlock.BLOCK_HGAP, basicCheck, indeterminateCheck)
); );
addSection("Usage", usageExample());
addSection("Indeterminate", indeterminateExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var cb1 = new CheckBox("_Unchecked");
cb1.setMnemonicParsing(true);
var cb2 = new CheckBox("Checked");
cb2.setSelected(true);
//snippet_1:end
var box = new HBox(40, cb1, cb2);
basicCheck = cb1;
var description = BBCodeParser.createFormattedText("""
A [i]CheckBox[/i] can be in one of three states:
[ul]
[li][b]checked[/b]: indeterminate == false, checked == true[/li]
[li][b]unchecked[/b]: indeterminate == false, checked == false[/li]
[li][b]undefined[/b]: indeterminate == true[/li][/ul]"""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox indeterminateExample() {
//snippet_2:start
var cb1 = new CheckBox("I_ndeterminate");
cb1.setAllowIndeterminate(true);
cb1.setIndeterminate(true);
cb1.setMnemonicParsing(true);
var cb2 = new CheckBox("Indeterminate + Checked");
cb2.setAllowIndeterminate(true);
cb2.setIndeterminate(false);
cb2.setSelected(true);
var cb3 = new CheckBox("Indeterminate + Unchecked");
cb3.setAllowIndeterminate(true);
//snippet_2:end
var box = new HBox(40, cb1, cb2, cb3);
indeterminateCheck = cb1;
var description = BBCodeParser.createFormattedText("""
The [code]allowIndeterminate[/code] variable, if true, allows the user to \
cycle through the undefined state. A [i]CheckBox[/i] is undefined if \
indeterminate is true, regardless of the state of selected."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
// visually compare normal and indeterminate checkboxes size // visually compare normal and indeterminate checkboxes size

@ -0,0 +1,13 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
public class ChoiceBoxPage extends ComboBoxPage {
public static final String NAME = "ChoiceBox";
@Override
public String getName() {
return NAME;
}
}

@ -2,22 +2,16 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP; import atlantafx.base.util.BBCodeParser;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.HPos;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ColorPicker; import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.HBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
public class ColorPickerPage extends AbstractPage { public class ColorPickerPage extends OutlinePage {
public static final String NAME = "ColorPicker"; public static final String NAME = "ColorPicker";
@ -28,79 +22,68 @@ public class ColorPickerPage extends AbstractPage {
public ColorPickerPage() { public ColorPickerPage() {
super(); super();
setUserContent(new VBox(
new SampleBlock("Playground", createPlayground()) addFormattedText("""
)); ColorPicker control allows the user to select a color from either \
a standard palette of colors with a simple one click selection or \
define their own custom color."""
);
addSection("Usage", usageExample());
addSection("Style", styleExample());
} }
private GridPane createPlayground() { private ExampleBox usageExample() {
var colorPicker = new ColorPicker(); //snippet_1:start
colorPicker.setValue(Color.DEEPSKYBLUE); var cp = new ColorPicker();
cp.setValue(Color.RED);
//snippet_1:end
var labelToggle = new ToggleSwitch(); var box = new HBox(cp);
labelToggle.setSelected(true); box.setMinHeight(50);
labelToggle.selectedProperty().addListener((obs, old, val) -> {
colorPicker.setStyle("-fx-color-label-visible: false;");
if (val) {
colorPicker.setStyle("-fx-color-label-visible: true;");
}
});
var disableToggle = new ToggleSwitch(); var description = BBCodeParser.createFormattedText("""
colorPicker.disableProperty().bind(disableToggle.selectedProperty()); The [i]ColorPicker[/i] control provides a color palette with a predefined \
set of colors. If the user does not want to choose from the predefined set, \
var grid = new GridPane(); they can create a custom color by interacting with a custom color dialog. \
grid.setHgap(BLOCK_HGAP); This dialog provides RGB, HSB and Web modes of interaction, to create new \
grid.setVgap(BLOCK_VGAP); colors. It also lets the opacity of the color to be modified."""
grid.add(colorPicker, 0, 0, 1, GridPane.REMAINING);
grid.add(createLabel("Show label"), 1, 0);
grid.add(labelToggle, 2, 0);
grid.add(createLabel("Picker style"), 1, 1);
grid.add(createPickerStyleChoice(colorPicker), 2, 1);
grid.add(createLabel("Disable"), 1, 2);
grid.add(disableToggle, 2, 2);
grid.getColumnConstraints().setAll(
new ColumnConstraints(200),
new ColumnConstraints(),
new ColumnConstraints()
); );
return grid; return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private Label createLabel(String text) { private ExampleBox styleExample() {
var label = new Label(text); //snippet_2:start
GridPane.setHalignment(label, HPos.RIGHT); var cp1 = new ColorPicker();
return label; cp1.setValue(Color.RED);
} cp1.getStyleClass().add(ColorPicker.STYLE_CLASS_BUTTON);
private ChoiceBox<String> createPickerStyleChoice(ColorPicker colorPicker) { var cp2 = new ColorPicker();
var optDefault = "Default"; cp2.setValue(Color.GREEN);
var optButton = "Button"; cp2.getStyleClass().add(ColorPicker.STYLE_CLASS_SPLIT_BUTTON);
var optSplitButton = "Split Button";
var choice = new ChoiceBox<String>(); var cp3 = new ColorPicker();
choice.getItems().setAll(optDefault, optButton, optSplitButton); cp3.setValue(Color.BLUE);
choice.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> { cp3.setStyle("-fx-color-label-visible: false");
if (val == null) { //snippet_2:end
return;
}
colorPicker.getStyleClass().removeAll( var grid = new GridPane();
ColorPicker.STYLE_CLASS_BUTTON, grid.setHgap(30);
ColorPicker.STYLE_CLASS_SPLIT_BUTTON grid.setVgap(10);
); grid.addRow(0,
captionLabel("STYLE_CLASS_BUTTON"),
captionLabel("STYLE_CLASS_SPLIT_BUTTON"),
captionLabel("-fx-color-label-visible")
);
grid.addRow(1, cp1, cp2, cp3);
if (optButton.equals(val)) { var description = BBCodeParser.createFormattedText("""
colorPicker.getStyleClass().add(ColorPicker.STYLE_CLASS_BUTTON); The [i]ColorPicker[/i] control can be styled in two ways: a simple \
} [i]Button[/i] mode or the default [i]MenuButton[/i] mode. While there \
if (optSplitButton.equals(val)) { is also a [i]SplitMenuButton[/i] mode available, it is not supported by \
colorPicker.getStyleClass().add(ColorPicker.STYLE_CLASS_SPLIT_BUTTON); AtlantaFX and looks the same as the default option."""
} );
});
choice.getSelectionModel().select(optDefault);
return choice; return new ExampleBox(grid, new Snippet(getClass(), 2), description);
} }
} }

@ -2,32 +2,25 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.STATE_DANGER; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static atlantafx.sampler.util.Containers.H_GROW_NEVER;
import static javafx.collections.FXCollections.observableArrayList;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.ExampleBox;
import java.util.function.Consumer; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell; import javafx.scene.control.ListCell;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.HBox;
import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class ComboBoxPage extends AbstractPage { public class ComboBoxPage extends OutlinePage {
public static final String NAME = "ComboBox"; public static final String NAME = "ComboBox";
private static final int PREF_WIDTH = 200; private static final int PREF_WIDTH = 200;
@ -39,177 +32,258 @@ public class ComboBoxPage extends AbstractPage {
public ComboBoxPage() { public ComboBoxPage() {
super(); super();
setUserContent(new VBox(
new SampleBlock("Examples", createPlayground()) addFormattedText("""
A user interface component which shows a list of items out of which \
user can select at most one item. JavaFX provides two pretty similar \
controls for that purpose, namely the [i]ComboBox[/i] and [i]ChoiceBox[/i]."""
);
addSection("Usage", usageExample());
addSection("Editable", editableExample());
addSection("Placeholder", placeholderExample());
addSection("Custom Items", customItemsExample());
addSection("Color", colorExample());
addSection("Overflow", overflowExample());
addSection("Alternative Icon", altIconExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var cmb1 = new ComboBox<String>();
cmb1.setPrefWidth(PREF_WIDTH);
cmb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
)); ));
} cmb1.getSelectionModel().selectFirst();
var cmb2 = new ComboBox<String>();
cmb2.setPrefWidth(PREF_WIDTH);
var chb1 = new ChoiceBox<String>();
chb1.setPrefWidth(PREF_WIDTH);
chb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
chb1.getSelectionModel().selectFirst();
var chb2 = new ChoiceBox<String>();
chb2.setPrefWidth(PREF_WIDTH);
//snippet_1:end
private GridPane createPlayground() {
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(BLOCK_HGAP); grid.setHgap(30);
grid.setVgap(BLOCK_VGAP); grid.setVgap(10);
grid.getColumnConstraints().setAll(H_GROW_NEVER, H_GROW_NEVER, H_GROW_NEVER); grid.addRow(0, captionLabel("ComboBox"), cmb1, cmb2);
grid.addRow(1, captionLabel("ChoiceBox"), chb1, chb2);
var comboLabel = new Label("C_omboBox"); var description = BBCodeParser.createFormattedText("""
comboLabel.setMnemonicParsing(true); The [i]ComboBox[/i] is an implementation of the [i]ComboBoxBase[/i] abstract class, \
grid.add(comboLabel, 0, 0); whereas the [i]CheckBox[/i] is more similar to the [i]MenuButton[/i]. Both controls \
provide a selection model to manage the selected state."""
);
var choiceLabel = new Label("C_hoiceBox"); return new ExampleBox(grid, new Snippet(ComboBoxPage.class, 1), description);
choiceLabel.setMnemonicParsing(true);
grid.add(choiceLabel, 2, 0);
// default
grid.add(createComboBox(), 0, 1);
grid.add(createLabel("empty"), 1, 1);
grid.add(createChoiceBox(), 2, 1);
// editable
grid.add(createComboBoxWith(c -> {
c.setItems(createItems(5));
c.setEditable(true);
}), 0, 2);
grid.add(createLabel("editable"), 1, 2);
// placeholder
grid.add(createComboBoxWith(c -> c.setPlaceholder(new Label("Loading..."))), 0, 3);
grid.add(createLabel("placeholder"), 1, 3);
// with icons
var badges = IntStream.range(0, 5).boxed()
.map(i -> new Badge(FAKER.hipster().word(), randomIcon()))
.collect(Collectors.toCollection(FXCollections::observableArrayList));
var badgeCombo = new ComboBox<>(badges);
badgeCombo.setPrefWidth(PREF_WIDTH);
badgeCombo.setButtonCell(new BadgeCell());
badgeCombo.setCellFactory(lv -> new BadgeCell());
badgeCombo.getSelectionModel().selectFirst();
grid.add(badgeCombo, 0, 4);
grid.add(createLabel("graphic"), 1, 4);
// success
grid.add(createComboBoxWith(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_SUCCESS, true);
c.getSelectionModel().selectFirst();
}), 0, 5);
grid.add(createLabel("success"), 1, 5);
grid.add(createChoiceBoxWith(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_SUCCESS, true);
c.getSelectionModel().selectFirst();
}), 2, 5);
// negative
grid.add(createComboBoxWith(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_DANGER, true);
c.getSelectionModel().selectFirst();
}), 0, 6);
grid.add(createLabel("success"), 1, 6);
grid.add(createChoiceBoxWith(c -> {
c.setItems(createItems(5));
c.pseudoClassStateChanged(STATE_DANGER, true);
c.getSelectionModel().selectFirst();
}), 2, 6);
// alt icon
grid.add(createComboBoxWith(c -> {
c.setItems(createItems(5));
c.getStyleClass().add(Tweaks.ALT_ICON);
c.getSelectionModel().selectFirst();
}), 0, 7);
grid.add(createLabel("alt icon"), 1, 7);
grid.add(createChoiceBoxWith(c -> {
c.setItems(createItems(5));
c.getStyleClass().add(Tweaks.ALT_ICON);
c.getSelectionModel().selectFirst();
}), 2, 7);
// disabled
grid.add(createComboBoxWith(c -> c.setDisable(true)), 0, 8);
grid.add(createLabel("disabled"), 1, 8);
grid.add(createChoiceBoxWith(c -> c.setDisable(true)), 2, 8);
// vertical overflow
grid.add(createComboBoxWith(c -> {
c.setItems(createItems(50));
c.getSelectionModel().selectFirst();
}), 0, 9);
grid.add(createLabel("large list"), 1, 9);
grid.add(createChoiceBoxWith(c -> {
c.setItems(createItems(50));
c.getSelectionModel().selectFirst();
}), 2, 9);
// horizontal overflow
grid.add(createComboBoxWith(c -> {
c.setItems(observableArrayList(generate(() -> FAKER.chuckNorris().fact(), 5)));
c.getSelectionModel().selectFirst();
}), 0, 10);
grid.add(createLabel("wide text"), 1, 10);
grid.add(createChoiceBoxWith(c -> {
c.setItems(observableArrayList(generate(() -> FAKER.chuckNorris().fact(), 5)));
c.getSelectionModel().selectFirst();
}), 2, 10);
return grid;
} }
private Label createLabel(String text) { private ExampleBox editableExample() {
var label = new Label(text); //snippet_2:start
GridPane.setHalignment(label, HPos.CENTER); var cmb = new ComboBox<String>();
return label; cmb.setPrefWidth(PREF_WIDTH);
cmb.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
cmb.getSelectionModel().selectFirst();
cmb.setEditable(true);
//snippet_2:end
var box = new HBox(cmb);
var description = BBCodeParser.createFormattedText("""
The [i]ComboBox[/i] provides a way for end-users to input values \
that are not available as options."""
);
return new ExampleBox(box, new Snippet(ComboBoxPage.class, 2), description);
} }
private ComboBox<String> createComboBox() { private ExampleBox placeholderExample() {
return createComboBoxWith(null); //snippet_3:start
var cmb = new ComboBox<String>();
cmb.setPrefWidth(PREF_WIDTH);
cmb.setPlaceholder(new Label("Loading..."));
//snippet_3:end
var box = new HBox(cmb);
var description = BBCodeParser.createFormattedText("""
Placeholder is a node that is shown to the user when the [i]ComboBox[/i] \
has no content to display."""
);
return new ExampleBox(box, new Snippet(ComboBoxPage.class, 3), description);
} }
private ComboBox<String> createComboBoxWith(Consumer<ComboBox<String>> mutator) { private ExampleBox customItemsExample() {
var c = new ComboBox<String>(); //snippet_4:start
c.setPrefWidth(PREF_WIDTH); record Badge(String text, Ikon icon) {
if (mutator != null) {
mutator.accept(c);
} }
return c;
}
private ChoiceBox<String> createChoiceBox() { class BadgeCell extends ListCell<Badge> {
return createChoiceBoxWith(null);
}
private ChoiceBox<String> createChoiceBoxWith(Consumer<ChoiceBox<String>> mutator) { @Override
var c = new ChoiceBox<String>(); protected void updateItem(Badge badge, boolean isEmpty) {
c.setPrefWidth(PREF_WIDTH); super.updateItem(badge, isEmpty);
if (mutator != null) {
mutator.accept(c);
}
return c;
}
private ObservableList<String> createItems(int count) { if (isEmpty) {
return observableArrayList(generate(() -> FAKER.hipster().word(), count)); setGraphic(null);
} setText(null);
} else {
/////////////////////////////////////////////////////////////////////////// setGraphic(new FontIcon(badge.icon()));
setText(badge.text());
@SuppressWarnings("unused") }
private record Badge(String text, Ikon icon) {
}
private static class BadgeCell extends ListCell<Badge> {
@Override
protected void updateItem(Badge item, boolean isEmpty) {
super.updateItem(item, isEmpty);
if (isEmpty) {
setGraphic(null);
setText(null);
} else {
setGraphic(new FontIcon(item.icon()));
setText(item.text());
} }
} }
var items = IntStream.range(0, 5).boxed()
.map(i -> new Badge(FAKER.hipster().word(), randomIcon()))
.collect(Collectors.toCollection(
FXCollections::observableArrayList
));
var cmb = new ComboBox<Badge>(items);
cmb.setPrefWidth(PREF_WIDTH);
cmb.setButtonCell(new BadgeCell());
cmb.setCellFactory(c -> new BadgeCell());
cmb.getSelectionModel().selectFirst();
//snippet_4:end
var box = new HBox(cmb);
var description = BBCodeParser.createFormattedText("""
The [i]ComboBox[/i] provides a custom cell factory that allows for \
complete customization of how the items are rendered."""
);
return new ExampleBox(box, new Snippet(ComboBoxPage.class, 4), description);
}
private ExampleBox colorExample() {
//snippet_5:start
var cmb1 = new ComboBox<String>();
cmb1.setPrefWidth(PREF_WIDTH);
cmb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
cmb1.getSelectionModel().selectFirst();
cmb1.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
var cmb2 = new ComboBox<String>();
cmb2.setPrefWidth(PREF_WIDTH);
cmb2.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
cmb2.getSelectionModel().selectFirst();
cmb2.pseudoClassStateChanged(Styles.STATE_DANGER, true);
var chb1 = new ChoiceBox<String>();
chb1.setPrefWidth(PREF_WIDTH);
chb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
chb1.getSelectionModel().selectFirst();
chb1.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
var chb2 = new ChoiceBox<String>();
chb2.setPrefWidth(PREF_WIDTH);
chb2.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
chb2.getSelectionModel().selectFirst();
chb2.pseudoClassStateChanged(Styles.STATE_DANGER, true);
//snippet_5:end
var grid = new GridPane();
grid.setHgap(30);
grid.setVgap(10);
grid.addRow(0, captionLabel("ComboBox"), cmb1, cmb2);
grid.addRow(1, captionLabel("ChoiceBox"), chb1, chb2);
var description = BBCodeParser.createFormattedText("""
You can use [code]Styles.STATE_SUCCESS[/code] or [code]Styles.STATE_DANGER[/code] \
pseudo-classes to change the control color. This especially useful to indicate \
the validation result."""
);
return new ExampleBox(grid, new Snippet(ComboBoxPage.class, 5), description);
}
private ExampleBox overflowExample() {
//snippet_7:start
var cmb1 = new ComboBox<String>();
cmb1.setPrefWidth(PREF_WIDTH);
cmb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 50)
));
cmb1.getSelectionModel().selectFirst();
var cmb2 = new ComboBox<String>();
cmb2.setPrefWidth(PREF_WIDTH);
cmb2.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.chuckNorris().fact(), 10)
));
cmb2.getSelectionModel().selectFirst();
var chb1 = new ChoiceBox<String>();
chb1.setPrefWidth(PREF_WIDTH);
chb1.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 50)
));
chb1.getSelectionModel().selectFirst();
var chb2 = new ChoiceBox<String>();
chb2.setPrefWidth(PREF_WIDTH);
chb2.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.chuckNorris().fact(), 10)
));
chb2.getSelectionModel().selectFirst();
//snippet_7:end
var grid = new GridPane();
grid.setHgap(30);
grid.setVgap(10);
grid.addRow(0, captionLabel("ComboBox"), cmb1, cmb2);
grid.addRow(1, captionLabel("ChoiceBox"), chb1, chb2);
var description = BBCodeParser.createFormattedText("""
This is just a simple example to test what happens when the item size \
exceeds the limits of the popup window."""
);
return new ExampleBox(grid, new Snippet(ComboBoxPage.class, 7), description);
}
private ExampleBox altIconExample() {
//snippet_6:start
var cmb = new ComboBox<String>();
cmb.setPrefWidth(PREF_WIDTH);
cmb.getStyleClass().add(Tweaks.ALT_ICON);
cmb.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
cmb.getSelectionModel().selectFirst();
var chb = new ChoiceBox<String>();
chb.setPrefWidth(PREF_WIDTH);
chb.getStyleClass().add(Tweaks.ALT_ICON);
chb.setItems(FXCollections.observableArrayList(
generate(() -> FAKER.hipster().word(), 5)
));
chb.getSelectionModel().selectFirst();
//snippet_6:end
var box = new HBox(30, cmb, chb);
var description = BBCodeParser.createFormattedText("""
There's additional tweak [code]Tweaks.ALT_ICON[/code] to change the control \
dropdown icon."""
);
return new ExampleBox(box, new Snippet(ComboBoxPage.class, 6), description);
} }
} }

@ -0,0 +1,88 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class ContextMenuPage extends AbstractPage {
public static final String NAME = "ContextMenu";
@Override
public String getName() {
return NAME;
}
public ContextMenuPage() {
super();
addFormattedText("""
A popup control containing a list of menu items. It allows for any [i]MenuItem[/i], \
including its subclasses, to be inserted. A common use case for this class is \
creating and showing context menus to users."""
);
addNode(contextMenuExample());
}
private Label contextMenuExample() {
var contextMenu = new ContextMenu();
var undoItem = createItem("_Undo", Feather.CORNER_DOWN_LEFT,
new KeyCodeCombination(KeyCode.Z, CONTROL_DOWN));
undoItem.setMnemonicParsing(true);
var redoItem = createItem("_Redo", Feather.CORNER_DOWN_RIGHT,
new KeyCodeCombination(KeyCode.Y, CONTROL_DOWN));
redoItem.setMnemonicParsing(true);
contextMenu.getItems().addAll(
undoItem,
redoItem,
new SeparatorMenuItem(),
createItem("Cut", Feather.SCISSORS, new KeyCodeCombination(KeyCode.X, CONTROL_DOWN)),
createItem("Copy", Feather.COPY, new KeyCodeCombination(KeyCode.C, CONTROL_DOWN)),
createItem("Paste", null, new KeyCodeCombination(KeyCode.V, CONTROL_DOWN))
);
var clickArea = new Label("Right-Click Here");
clickArea.setAlignment(Pos.CENTER);
clickArea.setMinSize(MAX_WIDTH / 2d, 200);
clickArea.setMaxSize(MAX_WIDTH / 2d, 200);
clickArea.setContextMenu(contextMenu);
clickArea.getStyleClass().add(Styles.BORDERED);
return clickArea;
}
private MenuItem createItem(@Nullable String text,
@Nullable Ikon graphic,
@Nullable KeyCombination accelerator) {
var item = new MenuItem(text);
if (graphic != null) {
item.setGraphic(new FontIcon(graphic));
}
if (accelerator != null) {
item.setAccelerator(accelerator);
}
return item;
}
}

@ -1,151 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.STATE_DANGER;
import static atlantafx.base.theme.Styles.STATE_SUCCESS;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.MaskTextField;
import atlantafx.base.controls.PasswordTextField;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
public class CustomTextFieldPage extends AbstractPage {
public static final String NAME = "CustomTextField";
private static final int PREF_WIDTH = 120;
@Override
public String getName() {
return NAME;
}
public CustomTextFieldPage() {
super();
setUserContent(new FlowPane(
PAGE_HGAP, PAGE_VGAP,
leftIconSample(),
rightIconSample(),
bothIconsSample(),
successSample(),
dangerSample(),
passwordSample(),
maskSample()
));
}
private SampleBlock leftIconSample() {
var tf = new CustomTextField();
tf.setPromptText("Prompt text");
tf.setRight(new FontIcon(Feather.X));
tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Left", tf);
}
private SampleBlock rightIconSample() {
var tf = new CustomTextField();
tf.setPromptText("Prompt text");
tf.setLeft(new FontIcon(Feather.MAP_PIN));
tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Right", tf);
}
private SampleBlock bothIconsSample() {
var tf = new CustomTextField("Text");
tf.setLeft(new FontIcon(Feather.MAP_PIN));
tf.setRight(new FontIcon(Feather.X));
tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Both Sides", tf);
}
private SampleBlock successSample() {
var tf = new CustomTextField("Text");
tf.pseudoClassStateChanged(STATE_SUCCESS, true);
tf.setRight(new FontIcon(Feather.X));
tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Success", tf);
}
private SampleBlock dangerSample() {
var tf = new CustomTextField();
tf.pseudoClassStateChanged(STATE_DANGER, true);
tf.setLeft(new FontIcon(Feather.MAP_PIN));
tf.setPrefWidth(PREF_WIDTH);
return new SampleBlock("Danger", tf);
}
private SampleBlock passwordSample() {
var tf = new PasswordTextField("qwerty");
tf.setPrefWidth(PREF_WIDTH);
var icon = new FontIcon(Feather.EYE_OFF);
icon.setCursor(Cursor.HAND);
icon.setOnMouseClicked(e -> {
if (tf.revealPasswordProperty().get()) {
tf.revealPasswordProperty().set(false);
icon.setIconCode(Feather.EYE_OFF);
} else {
tf.revealPasswordProperty().set(true);
icon.setIconCode(Feather.EYE);
}
});
tf.setRight(icon);
return new SampleBlock("Password", tf);
}
private SampleBlock maskSample() {
var phoneField = new MaskTextField("(999) 999 99 99");
phoneField.setPromptText("(999) 999 99 99");
phoneField.setLeft(new FontIcon(Material2OutlinedMZ.PHONE));
phoneField.setPrefWidth(180);
var cardField = new MaskTextField("9999-9999-9999-9999");
cardField.setLeft(new FontIcon(Material2OutlinedAL.CREDIT_CARD));
cardField.setPrefWidth(200);
var timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
var timeField = new MaskTextField("29:59");
timeField.setText(LocalTime.now(ZoneId.systemDefault()).format(timeFormatter));
timeField.setLeft(new FontIcon(Material2OutlinedMZ.TIMER));
timeField.setPrefWidth(120);
timeField.textProperty().addListener((obs, old, val) -> {
if (val != null) {
try {
//noinspection ResultOfMethodCallIgnored
LocalTime.parse(val, timeFormatter);
timeField.pseudoClassStateChanged(STATE_DANGER, false);
} catch (DateTimeParseException e) {
timeField.pseudoClassStateChanged(STATE_DANGER, true);
}
}
});
var content = new HBox(
BLOCK_HGAP,
new VBox(5, new Label("Phone Number"), phoneField),
new VBox(5, new Label("Bank Card"), cardField),
new VBox(5, new Label("Time"), timeField)
);
content.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Input Mask", content);
}
}

@ -2,334 +2,111 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import atlantafx.base.util.BBCodeParser;
import static atlantafx.base.theme.Styles.FLAT; import atlantafx.sampler.page.ExampleBox;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; import atlantafx.sampler.page.OutlinePage;
import static javafx.scene.layout.GridPane.REMAINING; import atlantafx.sampler.page.Snippet;
import atlantafx.base.controls.InlineDatePicker;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.chrono.HijrahChronology; import java.time.chrono.HijrahChronology;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Objects; import java.time.format.DateTimeParseException;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.DateCell;
import javafx.scene.control.DatePicker; import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class DatePickerPage extends AbstractPage { public class DatePickerPage extends OutlinePage {
public static final String NAME = "DatePicker"; public static final String NAME = "DatePicker";
private static final LocalDate TODAY = LocalDate.now(ZoneId.systemDefault());
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE;
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final String DATE_FORMATTER_PROMPT = "yyyy-MM-dd";
private static final int INLINE_DATE_PICKER_COL = 0;
private static final int INLINE_DATE_PICKER_ROW = 4;
private static final CSSFragment NO_YEAR_MONTH_STYLE = new CSSFragment("""
.date-picker-popup >.month-year-pane {
visibility: hidden;
-fx-min-width: 0;
-fx-pref-width: 0;
-fx-max-width: 0;
-fx-min-height: 0;
-fx-pref-height: 0;
-fx-max-height: 0;
}
""");
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;
} }
private final BooleanProperty weekNumProperty = new SimpleBooleanProperty();
private final BooleanProperty showClockProperty = new SimpleBooleanProperty();
private final BooleanProperty editableProperty = new SimpleBooleanProperty();
private final BooleanProperty offPastDatesProperty = new SimpleBooleanProperty();
private final BooleanProperty disableProperty = new SimpleBooleanProperty();
private final Clock clock = new Clock();
private DatePickerColorSelector colorSelector;
public DatePickerPage() { public DatePickerPage() {
super(); super();
setUserContent(new VBox(
new SampleBlock("Playground", createPlayground()) addFormattedText("""
)); A date picker control that allows the user to enter a date as text or to select \
a date from a calendar popup. The calendar is based on either the standard \
ISO-8601 chronology or any of the other chronology classes defined in the \
[code]java.time.chrono[/code] package."""
);
addSection("Usage", usageExample());
addSection("Editable", editableExample());
} }
private GridPane createPlayground() { public ExampleBox usageExample() {
//snippet_1:start
var today = LocalDate.now(ZoneId.systemDefault());
var dp1 = new DatePicker(today);
dp1.setPrefWidth(200);
var dp2 = new DatePicker(today.plusDays(10));
dp2.setShowWeekNumbers(true);
dp2.setPrefWidth(200);
var dp3 = new DatePicker(today.plusMonths(1));
dp3.setChronology(HijrahChronology.INSTANCE);
dp3.setPrefWidth(200);
//snippet_1:end
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(40); grid.setHgap(50);
grid.setVgap(BLOCK_VGAP); grid.setVgap(10);
grid.addRow(0, captionLabel("Default"), dp1);
grid.addRow(1, captionLabel("Week Numbers"), dp2);
grid.addRow(2, captionLabel("Second Chronology"), dp3);
final var popupDatePicker = createPopupDatePicker(); var description = BBCodeParser.createFormattedText("""
The [i]DatePicker[/i] control consists of a combo box with a date \
field and a date chooser."""
);
colorSelector = new DatePickerColorSelector(grid); return new ExampleBox(grid, new Snippet(getClass(), 1), description);
final var inlineDatePicker = createInlineDatePicker(null); }
// == CONTROLS == public ExampleBox editableExample() {
//snippet_2:start
final var today = LocalDate.now(ZoneId.systemDefault());
final var formatter = DateTimeFormatter.ISO_DATE;
var weekNumToggle = new ToggleSwitch("Week numbers"); var dp = new DatePicker(today);
weekNumProperty.bind(weekNumToggle.selectedProperty()); dp.setPromptText("yyyy-MM-dd");
weekNumToggle.setSelected(true); dp.setEditable(true);
dp.setPrefWidth(200);
dp.setConverter(new StringConverter<>() {
@Override
public String toString(LocalDate localDate) {
if (localDate == null) {
return "";
}
return formatter.format(localDate);
}
var showClockToggle = new ToggleSwitch("Show clock"); @Override
showClockProperty.bind(showClockToggle.selectedProperty()); public LocalDate fromString(String dateString) {
showClockToggle.setSelected(true); if (dateString == null || dateString.trim().isEmpty()) {
return today;
var showYearMonthToggle = new ToggleSwitch("Show header"); }
showYearMonthToggle.setSelected(true); try {
showYearMonthToggle.selectedProperty().addListener((obs, old, val) -> { return LocalDate.parse(dateString, formatter);
if (!val) { } catch (DateTimeParseException e) {
NO_YEAR_MONTH_STYLE.addTo(grid); return today;
} else { }
NO_YEAR_MONTH_STYLE.removeFrom(grid);
} }
}); });
//snippet_2:end
var chronologyToggle = new ToggleSwitch("Second chronology"); var box = new HBox(dp);
chronologyToggle.selectedProperty().addListener( var description = BBCodeParser.createFormattedText("""
(obs, old, val) -> popupDatePicker.setChronology(val ? HijrahChronology.INSTANCE : null) The [code]editable[/code] property controls whether the [i]DatePicker[/i] \
allows users to manually input a date."""
); );
var editableToggle = new ToggleSwitch("Editable"); return new ExampleBox(box, new Snippet(getClass(), 2), description);
editableProperty.bind(editableToggle.selectedProperty());
// clear selected value to demonstrate prompt text
editableProperty.addListener(
(obs, old, val) -> popupDatePicker.setValue(val ? null : TODAY)
);
var offPastDatesToggle = new ToggleSwitch("No past dates");
offPastDatesProperty.bind(offPastDatesToggle.selectedProperty());
offPastDatesProperty.addListener((obs, old, val) -> {
popupDatePicker.setDayCellFactory(val ? dp -> new FutureDateCell() : null);
popupDatePicker.setValue(TODAY);
// we have to create new date picker, because changing cell factory won't update existing cells
var datePicker = createInlineDatePicker(val ? dp -> new FutureDateCell() : null);
grid.getChildren().removeIf(n -> n instanceof InlineDatePicker);
grid.add(datePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
});
var disablePickerToggle = new ToggleSwitch("Disable");
disableProperty.bind(disablePickerToggle.selectedProperty());
var controls = new VBox(
BLOCK_VGAP,
weekNumToggle,
showClockToggle,
showYearMonthToggle,
chronologyToggle,
editableToggle,
offPastDatesToggle,
disablePickerToggle
);
controls.setAlignment(Pos.CENTER_RIGHT);
// == GRID ==
var defaultLabel = new Label("Default");
defaultLabel.getStyleClass().add(Styles.TEXT_BOLD);
var inlineLabel = new Label("Inline");
inlineLabel.getStyleClass().add(Styles.TEXT_BOLD);
grid.add(defaultLabel, 0, 0);
grid.add(popupDatePicker, 0, 1);
grid.add(new Spacer(20), 0, 2);
grid.add(inlineLabel, 0, 3);
grid.add(inlineDatePicker, INLINE_DATE_PICKER_COL, INLINE_DATE_PICKER_ROW);
grid.add(controls, 1, 0, 1, REMAINING);
return grid;
}
private DatePicker createPopupDatePicker() {
var datePicker = new DatePicker();
datePicker.setConverter(DATE_CONVERTER);
datePicker.setPromptText(DATE_FORMATTER_PROMPT);
datePicker.setMaxWidth(Double.MAX_VALUE);
datePicker.setValue(TODAY);
datePicker.showWeekNumbersProperty().bind(weekNumProperty);
datePicker.editableProperty().bind(editableProperty);
datePicker.disableProperty().bind(disableProperty);
return datePicker;
}
private InlineDatePicker createInlineDatePicker(Callback<InlineDatePicker, DateCell> dayCellFactory) {
var datePicker = new InlineDatePicker();
datePicker.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
datePicker.setDayCellFactory(dayCellFactory);
datePicker.setValue(TODAY);
datePicker.showWeekNumbersProperty().bind(weekNumProperty);
datePicker.disableProperty().bind(disableProperty);
datePicker.setTopNode(clock);
datePicker.setBottomNode(colorSelector);
datePicker.topNodeProperty().bind(Bindings.createObjectBinding(
() -> showClockProperty.get() ? clock : null, showClockProperty)
);
return datePicker;
}
///////////////////////////////////////////////////////////////////////////
private static final StringConverter<LocalDate> DATE_CONVERTER = new StringConverter<>() {
@Override
public String toString(LocalDate localDate) {
if (localDate == null) {
return "";
}
return DATE_FORMATTER.format(localDate);
}
@Override
public LocalDate fromString(String dateString) {
if (dateString == null || dateString.trim().isEmpty()) {
return null;
}
try {
return LocalDate.parse(dateString, DATE_FORMATTER);
} catch (Exception e) {
return null;
}
}
};
private static class FutureDateCell extends DateCell {
@Override
public void updateItem(LocalDate date, boolean empty) {
super.updateItem(date, empty);
setDisable(empty || date.isBefore(TODAY));
}
}
private static class Clock extends VBox {
public Clock() {
var clockLabel = new Label(
TIME_FORMATTER.format(LocalTime.now(ZoneId.systemDefault()))
);
clockLabel.getStyleClass().add(Styles.TITLE_2);
var dateLabel = new Label(
DateTimeFormatter.ofPattern("EEEE, LLLL dd, yyyy").format(LocalDate.now(ZoneId.systemDefault()))
);
setStyle("""
-fx-border-width: 0 0 0.5 0;
-fx-border-color: -color-border-default;"""
);
setSpacing(BLOCK_VGAP);
getChildren().setAll(clockLabel, dateLabel);
var t = new Timeline(new KeyFrame(
Duration.seconds(1),
e -> clockLabel.setText(
TIME_FORMATTER.format(LocalTime.now(ZoneId.systemDefault()))
)
));
t.setCycleCount(Animation.INDEFINITE);
t.playFromStart();
}
}
// This class shares stylesheet with the AccentColorSelector
private static class DatePickerColorSelector extends HBox {
private final Pane parent;
private final ObjectProperty<CSSFragment> style = new SimpleObjectProperty<>();
public DatePickerColorSelector(Pane parent) {
super();
this.parent = Objects.requireNonNull(parent);
createView();
}
private void createView() {
var resetBtn = new Button("", new FontIcon(Material2AL.CLEAR));
resetBtn.getStyleClass().addAll(BUTTON_ICON, FLAT);
resetBtn.setOnAction(e -> style.set(null));
style.addListener((obs, old, val) -> {
if (old != null) {
old.removeFrom(parent);
}
if (val != null) {
val.addTo(parent);
}
});
setAlignment(Pos.CENTER);
getChildren().setAll(
colorButton("-color-accent-emphasis", "-color-fg-emphasis"),
colorButton("-color-success-emphasis", "-color-fg-emphasis"),
colorButton("-color-danger-emphasis", "-color-fg-emphasis"),
resetBtn
);
getStyleClass().add("color-selector");
}
private Button colorButton(String bgColorName, String fgColorName) {
var icon = new Region();
icon.getStyleClass().add("icon");
icon.setStyle("-color-primary:" + bgColorName + ";");
var btn = new Button("", icon);
btn.getStyleClass().addAll(BUTTON_ICON, FLAT, "color-button");
btn.setOnAction(e -> updateStyle(bgColorName, fgColorName));
return btn;
}
private void updateStyle(String bgColorName, String fgColorName) {
style.set(new CSSFragment(String.format("""
.date-picker-popup {
-color-date-border: %s;
-color-date-month-year-bg: %s;
-color-date-month-year-fg: %s;
}
.date-picker-popup >.top-node {
-fx-background-color: %s;
-color-fg-default: %s;
-color-border-default: %s;
-fx-border-color: %s;
}""",
bgColorName, bgColorName, fgColorName,
bgColorName, fgColorName, fgColorName, fgColorName
)));
}
} }
} }

@ -2,38 +2,30 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static javafx.scene.control.Alert.AlertType; import static javafx.scene.control.Alert.AlertType;
import static javafx.scene.control.ButtonBar.ButtonData; import static javafx.scene.control.ButtonBar.ButtonData;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.ArrayList; import java.util.List;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.control.ChoiceDialog; import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.control.TextInputDialog; import javafx.scene.control.TextInputDialog;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.StageStyle;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class DialogPage extends AbstractPage { public class DialogPage extends OutlinePage {
public static final String NAME = "Dialog"; public static final String NAME = "Dialog";
@ -42,96 +34,69 @@ public class DialogPage extends AbstractPage {
return NAME; return NAME;
} }
private final BooleanProperty showHeaderProperty = new SimpleBooleanProperty(true);
private final BooleanProperty minDecorationsProperty = new SimpleBooleanProperty(true);
public DialogPage() { public DialogPage() {
super(); super();
createView();
}
private void createView() { addFormattedText("""
var showHeaderToggle = new ToggleSwitch("Show header"); Dialog is a user interface component that allows to create dialog windows \
showHeaderProperty.bind(showHeaderToggle.selectedProperty()); that can be used to prompt users for information or to display messages or warnings."""
showHeaderToggle.setSelected(true);
var minDecorationsToggle = new ToggleSwitch("Minimum decorations");
minDecorationsProperty.bind(minDecorationsToggle.selectedProperty());
minDecorationsToggle.setSelected(true);
var controls = new HBox(BLOCK_HGAP, showHeaderToggle, minDecorationsToggle);
controls.setAlignment(Pos.CENTER);
var samples = new FlowPane(
PAGE_HGAP, PAGE_VGAP,
infoDialogSample(),
warningDialogSample(),
errorDialogSample(),
exceptionDialogSample(),
confirmationDialogSample(),
textInputDialogSample(),
choiceDialogSample()
); );
addSection("Notifications", notificationDialogExample());
setUserContent(new VBox( addSection("Exception Dialog", exceptionDialogExample());
10, addSection("Confirmation Dialog", confirmationDialogExample());
controls, addSection("Text Input Dialog", textInputDialogExample());
new Separator(Orientation.HORIZONTAL), addSection("Choice Dialog", choiceDialogExample());
samples addSection("No Header", notificationNoHeaderDialogExample());
));
} }
private SampleBlock infoDialogSample() { private ExampleBox notificationDialogExample() {
var button = new Button("Click", new FontIcon(Feather.INFO)); //snippet_1:start
button.setOnAction(e -> { var infoBtn = new Button("Info", new FontIcon(Feather.INFO));
infoBtn.setOnAction(e -> {
var alert = new Alert(AlertType.INFORMATION); var alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Information Dialog"); alert.setTitle("Information Dialog");
alert.setHeaderText(randomHeader()); alert.setHeaderText(FAKER.chuckNorris().fact());
alert.setContentText(FAKER.lorem().paragraph(3)); alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow()); alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait(); alert.showAndWait();
}); });
return new SampleBlock("Information", button); var warnBtn = new Button("Click", new FontIcon(Feather.ALERT_TRIANGLE));
} warnBtn.setOnAction(e -> {
private SampleBlock warningDialogSample() {
var button = new Button("Click", new FontIcon(Feather.ALERT_TRIANGLE));
button.setOnAction(e -> {
var alert = new Alert(AlertType.WARNING); var alert = new Alert(AlertType.WARNING);
alert.setTitle("Warning Dialog"); alert.setTitle("Warning Dialog");
alert.setHeaderText(randomHeader()); alert.setHeaderText(FAKER.chuckNorris().fact());
alert.setContentText(FAKER.lorem().paragraph(3)); alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow()); alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait(); alert.showAndWait();
}); });
return new SampleBlock("Warning", button); var errorBtn = new Button("Click", new FontIcon(Feather.X_CIRCLE));
} errorBtn.setOnAction(e -> {
private SampleBlock errorDialogSample() {
var button = new Button("Click", new FontIcon(Feather.X_CIRCLE));
button.setOnAction(e -> {
var alert = new Alert(AlertType.ERROR); var alert = new Alert(AlertType.ERROR);
alert.setTitle("Error Dialog"); alert.setTitle("Error Dialog");
alert.setHeaderText(randomHeader()); alert.setHeaderText(FAKER.chuckNorris().fact());
alert.setContentText(FAKER.lorem().paragraph(3)); alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow()); alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait(); alert.showAndWait();
}); });
//snippet_1:end
return new SampleBlock("Error", button); var box = new HBox(30, infoBtn, warnBtn, errorBtn);
var description = BBCodeParser.createFormattedText("""
Pre-built dialog types for displaying information, warnings, and errors."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock exceptionDialogSample() { private ExampleBox exceptionDialogExample() {
//snippet_2:start
var button = new Button("Click", new FontIcon(Feather.MEH)); var button = new Button("Click", new FontIcon(Feather.MEH));
button.setOnAction(e -> { button.setOnAction(e -> {
var alert = new Alert(AlertType.ERROR); var alert = new Alert(AlertType.ERROR);
alert.setTitle("Exception Dialog"); alert.setTitle("Exception Dialog");
alert.setHeaderText(randomHeader()); alert.setHeaderText(FAKER.chuckNorris().fact());
alert.setContentText(FAKER.lorem().paragraph(3)); alert.setContentText(FAKER.lorem().paragraph(3));
var exception = new RuntimeException(FAKER.chuckNorris().fact()); var exception = new RuntimeException(FAKER.chuckNorris().fact());
@ -155,75 +120,128 @@ public class DialogPage extends AbstractPage {
alert.getDialogPane().setExpandableContent(content); alert.getDialogPane().setExpandableContent(content);
alert.initOwner(getScene().getWindow()); alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait(); alert.showAndWait();
}); });
//snippet_2:end
return new SampleBlock("Exception", button); var description = BBCodeParser.createFormattedText("""
A custom dialog that is designed to display information about exceptions that \
are thrown in JavaFX applications."""
);
return new ExampleBox(new HBox(button), new Snippet(getClass(), 2), description);
} }
private SampleBlock confirmationDialogSample() { private ExampleBox confirmationDialogExample() {
var button = new Button("Click", new FontIcon(Feather.CHECK_SQUARE)); //snippet_3:start
var button = new Button(
"Click", new FontIcon(Feather.CHECK_SQUARE)
);
button.setOnAction(e -> { button.setOnAction(e -> {
var alert = new Alert(AlertType.CONFIRMATION); var alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle("Confirmation Dialog"); alert.setTitle("Confirmation Dialog");
alert.setHeaderText(randomHeader()); alert.setHeaderText(FAKER.chuckNorris().fact());
alert.setContentText(FAKER.lorem().paragraph(3)); alert.setContentText(FAKER.lorem().paragraph(3));
ButtonType yesBtn = new ButtonType("Yes", ButtonData.YES); ButtonType yesBtn = new ButtonType("Yes", ButtonData.YES);
ButtonType noBtn = new ButtonType("No", ButtonData.NO); ButtonType noBtn = new ButtonType("No", ButtonData.NO);
ButtonType cancelBtn = new ButtonType("Cancel", ButtonData.CANCEL_CLOSE); ButtonType cancelBtn = new ButtonType(
"Cancel", ButtonData.CANCEL_CLOSE
);
alert.getButtonTypes().setAll(yesBtn, noBtn, cancelBtn); alert.getButtonTypes().setAll(yesBtn, noBtn, cancelBtn);
alert.initOwner(getScene().getWindow()); alert.initOwner(getScene().getWindow());
alert.initStyle(getModality());
alert.showAndWait(); alert.showAndWait();
}); });
//snippet_3:end
return new SampleBlock("Confirmation", button); var description = BBCodeParser.createFormattedText("""
The confirmation alert type configures the [i]Alert[/i] dialog to appear in a way that \
suggests the content of the dialog is seeking confirmation from the user."""
);
return new ExampleBox(new HBox(button), new Snippet(getClass(), 3), description);
} }
private SampleBlock textInputDialogSample() { private ExampleBox textInputDialogExample() {
//snippet_4:start
var button = new Button("Click", new FontIcon(Feather.EDIT_2)); var button = new Button("Click", new FontIcon(Feather.EDIT_2));
button.setOnAction(e -> { button.setOnAction(e -> {
var dialog = new TextInputDialog(); var dialog = new TextInputDialog();
dialog.setTitle("Text Input Dialog"); dialog.setTitle("Text Input Dialog");
dialog.setHeaderText(randomHeader()); dialog.setHeaderText(FAKER.chuckNorris().fact());
dialog.setContentText("Enter your name:"); dialog.setContentText("Enter your name:");
dialog.initOwner(getScene().getWindow()); dialog.initOwner(getScene().getWindow());
dialog.initStyle(getModality());
dialog.showAndWait(); dialog.showAndWait();
}); });
//snippet_4:end
return new SampleBlock("Text Input", button); var description = BBCodeParser.createFormattedText(
"A dialog that shows a text input control to the user."
);
return new ExampleBox(new HBox(button), new Snippet(getClass(), 4), description);
} }
private SampleBlock choiceDialogSample() { private ExampleBox choiceDialogExample() {
//snippet_5:start
var button = new Button("Click", new FontIcon(Feather.LIST)); var button = new Button("Click", new FontIcon(Feather.LIST));
button.setOnAction(e -> { button.setOnAction(e -> {
var choices = new ArrayList<>(); var choices = List.of("A", "B", "C");
choices.add("A");
choices.add("B");
choices.add("C");
var dialog = new ChoiceDialog<>(choices.get(0), choices); var dialog = new ChoiceDialog<>(choices.get(0), choices);
dialog.setTitle("Choice Dialog"); dialog.setTitle("Choice Dialog");
dialog.setHeaderText(randomHeader()); dialog.setHeaderText(FAKER.chuckNorris().fact());
dialog.setContentText("Choose your letter:"); dialog.setContentText("Choose your letter:");
dialog.initOwner(getScene().getWindow()); dialog.initOwner(getScene().getWindow());
dialog.initStyle(getModality());
dialog.showAndWait(); dialog.showAndWait();
}); });
//snippet_5:end
return new SampleBlock("Choice", button); var description = BBCodeParser.createFormattedText("""
A dialog that shows a list of choices to the user, from which they can pick one item at most."""
);
return new ExampleBox(new HBox(button), new Snippet(getClass(), 5), description);
} }
private String randomHeader() { private ExampleBox notificationNoHeaderDialogExample() {
return showHeaderProperty.get() ? FAKER.chuckNorris().fact() : null; //snippet_6:start
} var infoBtn = new Button("Info", new FontIcon(Feather.INFO));
infoBtn.setOnAction(e -> {
var alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Information Dialog");
alert.setHeaderText(null);
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.showAndWait();
});
private StageStyle getModality() { var warnBtn = new Button("Click", new FontIcon(Feather.ALERT_TRIANGLE));
return minDecorationsProperty.get() ? StageStyle.UTILITY : StageStyle.DECORATED; warnBtn.setOnAction(e -> {
var alert = new Alert(AlertType.WARNING);
alert.setTitle("Warning Dialog");
alert.setHeaderText(null);
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.showAndWait();
});
var errorBtn = new Button("Click", new FontIcon(Feather.X_CIRCLE));
errorBtn.setOnAction(e -> {
var alert = new Alert(AlertType.ERROR);
alert.setTitle("Error Dialog");
alert.setHeaderText(null);
alert.setContentText(FAKER.lorem().paragraph(3));
alert.initOwner(getScene().getWindow());
alert.showAndWait();
});
//snippet_6:end
var box = new HBox(30, infoBtn, warnBtn, errorBtn);
var description = BBCodeParser.createFormattedText(
"The header text can be hidden."
);
return new ExampleBox(box, new Snippet(getClass(), 6), description);
} }
} }

@ -2,21 +2,18 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Theme; import atlantafx.base.theme.Theme;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.event.ThemeEvent; import atlantafx.sampler.event.ThemeEvent;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.HighlightJSTheme; import atlantafx.sampler.theme.HighlightJSTheme;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.scene.web.HTMLEditor; import javafx.scene.web.HTMLEditor;
public class HtmlEditorPage extends AbstractPage { public class HtmlEditorPage extends AbstractPage {
@ -30,13 +27,16 @@ public class HtmlEditorPage extends AbstractPage {
return NAME; return NAME;
} }
private HTMLEditor editor = createEditor(); private HTMLEditor editor = createHTMLEditor();
public HtmlEditorPage() { public HtmlEditorPage() {
super(); super();
setUserContent(new VBox(editorSample())); addFormattedText("""
editor.requestFocus(); A control that allows for users to edit text, and apply styling to this text. \
The underlying data model is HTML, although this is not shown visually to the end-user."""
);
addNode(editorSample());
// update editor colors on app theme change // update editor colors on app theme change
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> { DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
@ -45,42 +45,46 @@ public class HtmlEditorPage extends AbstractPage {
editor.requestFocus(); editor.requestFocus();
} }
}); });
editor.requestFocus();
} }
private SampleBlock editorSample() { private VBox editorSample() {
var description = new Text(""" var description = BBCodeParser.createFormattedText("""
HTMLEditor toolbar buttons use images from 'com/sun/javafx/scene/control/skin/modena'. Since AtlantaFX themes are also distributed as CSS files, they can't contain any images. \
In opposite, since AtlantaFX themes are also distributed as single CSS files, it contains no images. Unfortunately, reusing Modena resources in theme also isn't possible, because the they \
Unfortunately reusing Modena resources isn't possible, because the package isn't opened in OpenJFX are located in [font=monospace]'com/sun/javafx/*'[/font] package, which isn't opened in \
'module-info'. OpenJFX [font=monospace]'module-info'[/font]. But you can still copy Modena images and \
""" overwrite [i]HMTLEditor[/i] CSS in your app."""
); );
var fixToggle = new ToggleSwitch("Apply Fix"); var fixToggle = new ToggleSwitch("Apply Fix");
var content = new VBox(BLOCK_VGAP, editor, new TextFlow(description), fixToggle); var content = new VBox(20, editor, description, fixToggle);
content.setAlignment(Pos.CENTER); content.setAlignment(Pos.TOP_CENTER);
VBox.setVgrow(content, Priority.ALWAYS);
fixToggle.selectedProperty().addListener((obs, old, val) -> { fixToggle.selectedProperty().addListener((obs, old, val) -> {
// toolbar icons can't be changed back without creating new editor instance #javafx-bug // toolbar icons can't be changed back without creating new editor instance #javafx-bug
try { try {
editor = createEditor(); editor = createHTMLEditor();
editor.pseudoClassStateChanged(USE_LOCAL_URL, val); editor.pseudoClassStateChanged(USE_LOCAL_URL, val);
content.getChildren().set(0, editor); content.getChildren().set(0, editor);
editor.requestFocus(); editor.requestFocus();
} catch (Exception ignored) { } catch (Exception ignored) {
// hush internal HTML editor errors, because everything // hush internal HTML editor errors, because everything
// we do here is ugly hacks around legacy control anyway // we do here is an ugly hack around legacy control anyway
} }
}); });
return new SampleBlock("Playground", content); return content;
} }
private HTMLEditor createEditor() { private HTMLEditor createHTMLEditor() {
var editor = new HTMLEditor(); var editor = new HTMLEditor();
editor.setPrefHeight(400); editor.setPrefHeight(400);
editor.setHtmlText(generateContent()); editor.setHtmlText(generateContent());
VBox.setVgrow(editor, Priority.ALWAYS);
return editor; return editor;
} }
@ -88,6 +92,10 @@ public class HtmlEditorPage extends AbstractPage {
var tm = ThemeManager.getInstance(); var tm = ThemeManager.getInstance();
Theme samplerTheme = tm.getTheme(); Theme samplerTheme = tm.getTheme();
HighlightJSTheme hlTheme = tm.getMatchingSourceCodeHighlightTheme(samplerTheme); HighlightJSTheme hlTheme = tm.getMatchingSourceCodeHighlightTheme(samplerTheme);
var text = String.join("<br/><br/>", generate(
() -> String.join(" ", FAKER.lorem().paragraphs(5)), 5)
);
return "<!DOCTYPE html>" return "<!DOCTYPE html>"
+ "<html>" + "<html>"
+ "<body style=\"" + "<body style=\""
@ -96,7 +104,7 @@ public class HtmlEditorPage extends AbstractPage {
+ "font-family:" + tm.getFontFamily() + ";" + "font-family:" + tm.getFontFamily() + ";"
+ "font-size:" + tm.getFontSize() + "px;" + "font-size:" + tm.getFontSize() + "px;"
+ "\">" + "\">"
+ String.join("<br/><br/>", FAKER.lorem().paragraphs(10)) + text
+ "</body>" + "</body>"
+ "</html>"; + "</html>";
} }

@ -1,171 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class InputGroupPage extends AbstractPage {
public static final String NAME = "Input Group";
@Override
public String getName() {
return NAME;
}
public InputGroupPage() {
super();
setUserContent(new VBox(
Page.PAGE_VGAP,
expandingHBox(httpMethodSample(), passwordSample()),
expandingHBox(networkSample(), dropdownSample()),
labelSample()
));
}
private SampleBlock httpMethodSample() {
var leftCombo = new ComboBox<>();
leftCombo.getItems().addAll("POST", "GET", "PUT", "PATCH", "DELETE");
leftCombo.getStyleClass().add(Styles.LEFT_PILL);
leftCombo.getSelectionModel().selectFirst();
var rightText = new TextField("https://example.org");
rightText.getStyleClass().add(Styles.RIGHT_PILL);
var box = new HBox(leftCombo, rightText);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("ComboBox & TextField", box);
}
private SampleBlock passwordSample() {
var leftPassword = new TextField();
leftPassword.setText(FAKER.internet().password());
leftPassword.getStyleClass().add(Styles.LEFT_PILL);
var rightBtn = new Button("", new FontIcon(Feather.REFRESH_CW));
rightBtn.getStyleClass().addAll(BUTTON_ICON);
rightBtn.setOnAction(e -> leftPassword.setText(FAKER.internet().password()));
rightBtn.getStyleClass().add(Styles.RIGHT_PILL);
var box = new HBox(leftPassword, rightBtn);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Text Field & Button", box);
}
private SampleBlock networkSample() {
var leftText = new TextField("192.168.1.10");
leftText.getStyleClass().add(Styles.LEFT_PILL);
leftText.setPrefWidth(140);
var centerText = new TextField("24");
centerText.getStyleClass().add(Styles.CENTER_PILL);
centerText.setPrefWidth(70);
var rightText = new TextField("192.168.1.1");
rightText.getStyleClass().add(Styles.RIGHT_PILL);
rightText.setPrefWidth(140);
var box = new HBox(leftText, centerText, rightText);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Text Fields", box);
}
private SampleBlock dropdownSample() {
var rightText = new TextField(FAKER.harryPotter().spell());
rightText.getStyleClass().add(Styles.RIGHT_PILL);
var spellItem = new MenuItem("Spell");
spellItem.setOnAction(e -> rightText.setText(FAKER.harryPotter().spell()));
var characterItem = new MenuItem("Character");
characterItem.setOnAction(e -> rightText.setText(FAKER.harryPotter().character()));
var locationItem = new MenuItem("Location");
locationItem.setOnAction(e -> rightText.setText(FAKER.harryPotter().location()));
var leftMenu = new MenuButton("Generate");
leftMenu.getItems().addAll(spellItem, characterItem, locationItem);
leftMenu.getStyleClass().add(Styles.LEFT_PILL);
var box = new HBox(leftMenu, rightText);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("MenuButton & TextField", box);
}
private SampleBlock labelSample() {
var leftLabel1 = new Label("", new CheckBox());
leftLabel1.getStyleClass().add(Styles.LEFT_PILL);
var rightText1 = new TextField();
rightText1.setPromptText("Username");
rightText1.getStyleClass().add(Styles.RIGHT_PILL);
rightText1.setPrefWidth(100);
var sample1 = new HBox(leftLabel1, rightText1);
sample1.setAlignment(Pos.CENTER_LEFT);
// ~
var leftText2 = new TextField("johndoe");
leftText2.getStyleClass().add(Styles.LEFT_PILL);
leftText2.setPrefWidth(100);
var centerLabel2 = new Label("@");
centerLabel2.getStyleClass().add(Styles.CENTER_PILL);
var rightText2 = new TextField("gmail.com");
rightText2.getStyleClass().add(Styles.RIGHT_PILL);
rightText2.setPrefWidth(100);
var sample2 = new HBox(leftText2, centerLabel2, rightText2);
sample2.setAlignment(Pos.CENTER_LEFT);
// ~
var leftText3 = new TextField("+123456");
leftText3.getStyleClass().add(Styles.LEFT_PILL);
leftText3.setPrefWidth(100);
var rightLabel3 = new Label("", new FontIcon(Feather.DOLLAR_SIGN));
rightLabel3.getStyleClass().add(Styles.RIGHT_PILL);
var sample3 = new HBox(leftText3, rightLabel3);
sample3.setAlignment(Pos.CENTER_LEFT);
// ~
var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
sample1,
sample2,
sample3
);
return new SampleBlock("Label & TextField", flowPane);
}
}

@ -2,12 +2,9 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
@ -25,17 +22,16 @@ public class LabelPage extends AbstractPage {
public LabelPage() { public LabelPage() {
super(); super();
createView();
addFormattedText("""
Label is a non-editable text control. A [i]Label[/i] is useful for displaying text that \
is required to fit within a specific space, and thus may need to use an ellipsis \
or truncation to size the string to fit."""
);
addNode(colorExample());
} }
private void createView() { private VBox colorExample() {
setUserContent(new VBox(
PAGE_VGAP,
expandingHBox(colorSample())
));
}
private SampleBlock colorSample() {
var defaultLabel = new Label("default", createFontIcon()); var defaultLabel = new Label("default", createFontIcon());
var accentLabel = new Label("accent", createFontIcon()); var accentLabel = new Label("accent", createFontIcon());
@ -56,17 +52,23 @@ public class LabelPage extends AbstractPage {
var subtleLabel = new Label("subtle", createFontIcon()); var subtleLabel = new Label("subtle", createFontIcon());
subtleLabel.getStyleClass().add(Styles.TEXT_SUBTLE); subtleLabel.getStyleClass().add(Styles.TEXT_SUBTLE);
var content = new VBox( var description = BBCodeParser.createFormattedText("""
BLOCK_VGAP, You can use pseudo-classes to set the [i]Label[/i] color. Note that icon \
new Label("You can also use pseudo-classes to set Label color."), inherits label color by default."""
new Label("Note that icon inherits label color by default."), );
new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
defaultLabel, accentLabel, successLabel, warningLabel, dangerLabel,
mutedLabel, subtleLabel
));
return new SampleBlock("Colors", content); var labels = new FlowPane(
20, 20,
defaultLabel,
accentLabel,
successLabel,
warningLabel,
dangerLabel,
mutedLabel,
subtleLabel
);
return new VBox(20, description, labels);
} }
private FontIcon createFontIcon(String... stylesClass) { private FontIcon createFontIcon(String... stylesClass) {

@ -1,337 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT;
import static atlantafx.base.theme.Styles.BORDERED;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.STRIPED;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.fake.domain.Book;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.control.cell.ChoiceBoxListCell;
import javafx.scene.control.cell.ComboBoxListCell;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class ListPage extends AbstractPage {
public static final String NAME = "ListView";
@Override
public String getName() {
return NAME;
}
private final List<Book> dataList = generate(() -> Book.random(FAKER), 50);
private final StringConverter<Book> bookStringConverter = new BookStringConverter(dataList);
private final BorderPane listWrapper = new BorderPane();
private final ComboBox<Example> exampleSelect = createExampleSelect();
public ListPage() {
super();
var sample = new SampleBlock("Playground", createPlayground());
sample.setFillHeight(true);
setUserContent(sample);
}
private VBox createPlayground() {
var borderedToggle = new ToggleSwitch("Bordered");
borderedToggle.selectedProperty()
.addListener((obs, old, value) -> toggleListProperty(lv -> toggleStyleClass(lv, BORDERED)));
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty()
.addListener((obs, old, value) -> toggleListProperty(lv -> toggleStyleClass(lv, DENSE)));
var stripedToggle = new ToggleSwitch("Striped");
stripedToggle.selectedProperty()
.addListener((obs, old, value) -> toggleListProperty(lv -> toggleStyleClass(lv, STRIPED)));
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty()
.addListener((obs, old, value) -> toggleListProperty(lv -> toggleStyleClass(lv, Tweaks.EDGE_TO_EDGE)));
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedList().ifPresent(lv -> {
if (val != null) {
lv.setDisable(val);
}
}));
var controls = new HBox(BLOCK_HGAP, borderedToggle, denseToggle, stripedToggle, edge2edgeToggle);
controls.setAlignment(Pos.CENTER);
VBox.setVgrow(listWrapper, Priority.ALWAYS);
var playground = new VBox(
BLOCK_VGAP,
new HBox(new Label("Select an example:"), new Spacer(), disableToggle),
exampleSelect,
listWrapper,
controls
);
playground.setMinHeight(100);
return playground;
}
private ComboBox<Example> createExampleSelect() {
var select = new ComboBox<Example>();
select.setMaxWidth(Double.MAX_VALUE);
select.getItems().setAll(Example.values());
select.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
ListView<?> newList = createList(val);
// copy existing style classes and properties to the new list
findDisplayedList().ifPresent(lv -> {
List<String> currentStyles = lv.getStyleClass();
currentStyles.remove("list-view");
newList.getStyleClass().addAll(currentStyles);
newList.setDisable(lv.isDisable());
});
listWrapper.setCenter(newList);
});
select.setConverter(new StringConverter<>() {
@Override
public String toString(Example example) {
return example == null ? "" : example.getName();
}
@Override
public Example fromString(String s) {
return Example.find(s);
}
});
return select;
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private Optional<ListView<?>> findDisplayedList() {
return listWrapper.getChildren().size() > 0
? Optional.of((ListView<?>) listWrapper.getChildren().get(0))
: Optional.empty();
}
private void toggleListProperty(Consumer<ListView<?>> consumer) {
findDisplayedList().ifPresent(lv -> {
if (consumer != null) {
consumer.accept(lv);
}
});
}
private ListView<?> createList(Example example) {
switch (example) {
case TEXT -> {
return stringList();
}
case EDITABLE -> {
return editableList();
}
case CHECK_BOX -> {
return checkBoxList();
}
case CHOICE_BOX -> {
return choiceBoxList();
}
case COMBO_BOX -> {
return comboBoxList();
}
case NESTED_CONTROLS -> {
return nestedControlsList();
}
default -> throw new IllegalArgumentException("Unexpected enum value: " + example);
}
}
private ListView<String> stringList() {
var lv = new ListView<String>();
lv.getItems().setAll(dataList.stream().map(bookStringConverter::toString).collect(Collectors.toList()));
return lv;
}
private ListView<String> editableList() {
var lv = new ListView<String>();
lv.setEditable(true);
lv.setCellFactory(TextFieldListCell.forListView());
lv.getItems().setAll(
// small size to see the empty cells
dataList.stream().limit(5).map(bookStringConverter::toString).collect(Collectors.toList())
);
return lv;
}
private ListView<Book> checkBoxList() {
var lv = new ListView<Book>();
lv.setCellFactory(CheckBoxListCell.forListView(Book::stateProperty, bookStringConverter));
lv.getItems().setAll(dataList.stream().limit(10).collect(Collectors.toList()));
return lv;
}
private ListView<Book> choiceBoxList() {
var lv = new ListView<Book>();
lv.setEditable(true);
lv.setCellFactory(
ChoiceBoxListCell.forListView(bookStringConverter, dataList.subList(0, 10).toArray(Book[]::new)));
lv.getItems().setAll(dataList.stream().limit(10).collect(Collectors.toList()));
return lv;
}
private ListView<Book> comboBoxList() {
var lv = new ListView<Book>();
lv.setEditable(true);
lv.setCellFactory(
ComboBoxListCell.forListView(bookStringConverter, dataList.subList(0, 10).toArray(Book[]::new)));
lv.getItems().setAll(dataList.stream().limit(10).collect(Collectors.toList()));
return lv;
}
private ListView<Book> nestedControlsList() {
var lv = new ListView<Book>();
lv.setCellFactory(book -> new NestedControlsListCell());
lv.getItems().setAll(dataList.stream().limit(10).collect(Collectors.toList()));
return lv;
}
///////////////////////////////////////////////////////////////////////////
private enum Example {
TEXT("Text"),
EDITABLE("TextFieldListCell"),
CHECK_BOX("CheckBoxListCell"),
CHOICE_BOX("ChoiceBoxListCell"),
COMBO_BOX("ComboBoxListCell"),
NESTED_CONTROLS("Nested controls");
private final String name;
Example(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Example find(String name) {
return Arrays.stream(Example.values())
.filter(example -> Objects.equals(example.getName(), name))
.findFirst()
.orElse(null);
}
}
private static class BookStringConverter extends StringConverter<Book> {
private final List<Book> dataList;
public BookStringConverter(List<Book> dataList) {
this.dataList = dataList;
}
@Override
public String toString(Book book) {
if (book == null) {
return null;
}
return String.format("\"%s\" by %s", book.getTitle(), book.getAuthor());
}
@Override
public Book fromString(String s) {
if (s == null) {
return null;
}
int sep = s.indexOf("\" by");
String title = s.substring(1, sep);
String author = s.substring(sep + "\" by".length());
return dataList.stream()
.filter(b -> Objects.equals(b.getTitle(), title) && Objects.equals(b.getAuthor(), author))
.findFirst()
.orElse(null);
}
}
private static class NestedControlsListCell extends ListCell<Book> {
private final HBox root;
private final Label titleLabel;
private final Hyperlink authorLink;
public NestedControlsListCell() {
titleLabel = new Label();
authorLink = new Hyperlink();
var purchaseBtn = new Button("Purchase");
purchaseBtn.getStyleClass().addAll(ACCENT);
purchaseBtn.setGraphic(new FontIcon(Feather.SHOPPING_CART));
root = new HBox(5,
titleLabel,
new Label(" by"),
authorLink,
new Spacer(),
purchaseBtn
);
root.setAlignment(Pos.CENTER_LEFT);
}
@Override
public void updateItem(Book book, boolean empty) {
super.updateItem(book, empty);
if (empty) {
setGraphic(null);
return;
}
titleLabel.setText("\"" + book.getTitle() + "\"");
authorLink.setText(book.getAuthor());
setGraphic(root);
}
}
}

@ -0,0 +1,477 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.fake.domain.Book;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.control.cell.ChoiceBoxListCell;
import javafx.scene.control.cell.ComboBoxListCell;
import javafx.scene.control.cell.TextFieldListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class ListViewPage extends OutlinePage {
public static final String NAME = "ListView";
@Override
public String getName() {
return NAME;
}
public ListViewPage() {
super();
addFormattedText("""
A [i]ListView[/i] displays a horizontal or vertical list of items from which the \
user may select, or with which the user may interact. A ListView is able to \
have its generic type set to represent the type of data in the backing model.""");
addSection("Usage", usageExample());
addSection("Row Style", rowStyleExample());
addSection("Selection Color", selectionColorExample());
addSection("Edge-to-Edge", edge2EdgeExample());
addSection("Playground", playground());
}
private ExampleBox usageExample() {
//snippet_1:start
var names = FXCollections.observableArrayList("Julia", "Ian", "Sue");
var lv = new ListView<>(names);
lv.setMinHeight(200);
//snippet_1:end
var box = new VBox(20, lv);
var description = BBCodeParser.createFormattedText("""
You can create a list view by instantiating the \
[font=monospace]javafx.scene.control.ListView[/font] class."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox rowStyleExample() {
//snippet_2:start
var names = FXCollections.observableArrayList("Julia", "Ian", "Sue");
var lv = new ListView<>(names);
lv.setMinHeight(200);
var borderToggle = new ToggleSwitch("Bordered");
borderToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(lv, Styles.BORDERED)
);
var stripeToggle = new ToggleSwitch("Striped");
stripeToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(lv, Styles.STRIPED)
);
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(lv, Styles.DENSE)
);
//snippet_2:end
var togglesBox = new HBox(HGAP_20, borderToggle, stripeToggle, denseToggle);
togglesBox.setAlignment(Pos.CENTER);
var box = new VBox(VGAP_10, lv, togglesBox);
var description = BBCodeParser.createFormattedText("""
The [i]ListView[/i] rows can be styled simply by adding CSS classes:
[ul]
[li][code]Styles.BORDERED[/code] - adds borders between the rows.[/li]
[li][code]Styles.STRIPED[/code] - adds zebra-striping.[/li]
[li][code]Styles.DENSE[/code] - makes the [i]ListView[/i] more compact by cutting \
cell padding.[/li][/ul]"""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox selectionColorExample() {
var style = """
-color-cell-bg-selected: -color-accent-emphasis;
-color-cell-fg-selected: -color-fg-emphasis;
-color-cell-bg-selected-focused: -color-accent-emphasis;
-color-cell-fg-selected-focused: -color-fg-emphasis;""";
//snippet_3:start
var names = FXCollections.observableArrayList("Julia", "Ian", "Sue");
var lv = new ListView<>(names);
lv.setMinHeight(200);
// -color-cell-bg-selected: -color-accent-emphasis;
// -color-cell-fg-selected: -color-fg-emphasis;
// -color-cell-bg-selected-focused: -color-accent-emphasis;
// -color-cell-fg-selected-focused: -color-fg-emphasis;
lv.setStyle(style);
lv.getSelectionModel().selectFirst();
//snippet_3:end
var box = new HBox(lv);
var description = BBCodeParser.createFormattedText("""
Cell selection color (and more) can be changed via looked-up color variables. \
You can find all supported color variables in the docs."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox edge2EdgeExample() {
//snippet_5:start
var names = FXCollections.observableArrayList("Julia", "Ian", "Sue");
var lv = new ListView<>(names);
lv.setMinHeight(150);
lv.getStyleClass().add(Tweaks.EDGE_TO_EDGE);
//snippet_5:end
var box = new VBox(lv);
box.setStyle("""
-fx-border-color: -color-accent-emphasis;
-fx-border-width: 2px;"""
);
var description = BBCodeParser.createFormattedText("""
Use [code]Tweaks.EDGE_TO_EDGE[/code] style class to remove the [i]ListView[/i] \
outer borders. This is useful if you want to place the table into external \
container that already has its own borders."""
);
return new ExampleBox(box, new Snippet(getClass(), 5), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private final List<Book> dataList = generate(() -> Book.random(FAKER), 50);
private final StringConverter<Book> bookStringConverter = new BookStringConverter(dataList);
private final StackPane listWrapper = new StackPane();
private final ComboBox<Example> exampleSelect = createExampleSelect();
private VBox playground() {
var borderToggle = new ToggleSwitch("Bordered");
borderToggle.selectedProperty().addListener(
(obs, old, value) -> toggleListProperty(lv ->
Styles.toggleStyleClass(lv, Styles.BORDERED)
));
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, value) -> toggleListProperty(lv ->
Styles.toggleStyleClass(lv, Styles.DENSE)
));
var stripeToggle = new ToggleSwitch("Striped");
stripeToggle.selectedProperty().addListener(
(obs, old, value) -> toggleListProperty(lv ->
Styles.toggleStyleClass(lv, Styles.STRIPED)
));
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, value) -> toggleListProperty(lv ->
Styles.toggleStyleClass(lv, Tweaks.EDGE_TO_EDGE)
));
var controls = new HBox(
HGAP_20,
borderToggle, denseToggle, stripeToggle, edge2edgeToggle
);
controls.setAlignment(Pos.CENTER);
listWrapper.setMinHeight(400);
VBox.setVgrow(listWrapper, Priority.ALWAYS);
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]ListView[/i] features \
and also serves as an object for monkey testing."""
);
return new VBox(
VGAP_10,
description,
new HBox(new Label("Select an example:")),
exampleSelect,
listWrapper,
controls
);
}
private ComboBox<Example> createExampleSelect() {
var select = new ComboBox<Example>();
select.setMaxWidth(Double.MAX_VALUE);
select.getItems().setAll(Example.values());
select.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
ListView<?> newList = createList(val);
// copy existing style classes and properties to the new list
findDisplayedList().ifPresent(lv -> {
List<String> currentStyles = lv.getStyleClass();
currentStyles.remove("list-view");
newList.getStyleClass().addAll(currentStyles);
newList.setDisable(lv.isDisable());
});
listWrapper.getChildren().setAll(newList);
});
select.setConverter(new StringConverter<>() {
@Override
public String toString(Example example) {
return example == null ? "" : example.getName();
}
@Override
public Example fromString(String s) {
return Example.find(s);
}
});
return select;
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private Optional<ListView<?>> findDisplayedList() {
return listWrapper.getChildren().size() > 0
? Optional.of((ListView<?>) listWrapper.getChildren().get(0))
: Optional.empty();
}
private void toggleListProperty(Consumer<ListView<?>> consumer) {
findDisplayedList().ifPresent(lv -> {
if (consumer != null) {
consumer.accept(lv);
}
});
}
private ListView<?> createList(Example example) {
switch (example) {
case TEXT -> {
return stringList();
}
case EDITABLE -> {
return editableList();
}
case CHECK_BOX -> {
return checkBoxList();
}
case CHOICE_BOX -> {
return choiceBoxList();
}
case COMBO_BOX -> {
return comboBoxList();
}
case NESTED_CONTROLS -> {
return nestedControlsList();
}
default -> throw new IllegalArgumentException("Unexpected enum value: " + example);
}
}
private ListView<String> stringList() {
var lv = new ListView<String>();
lv.getItems().setAll(dataList.stream()
.map(bookStringConverter::toString).collect(Collectors.toList())
);
return lv;
}
private ListView<String> editableList() {
var lv = new ListView<String>();
lv.setEditable(true);
lv.setCellFactory(TextFieldListCell.forListView());
lv.getItems().setAll(
// small size to see the empty cells
dataList.stream().limit(5).map(
bookStringConverter::toString).collect(Collectors.toList())
);
return lv;
}
private ListView<Book> checkBoxList() {
var lv = new ListView<Book>();
lv.setCellFactory(
CheckBoxListCell.forListView(Book::stateProperty, bookStringConverter)
);
lv.getItems().setAll(
dataList.stream().limit(10).collect(Collectors.toList())
);
return lv;
}
private ListView<Book> choiceBoxList() {
var lv = new ListView<Book>();
lv.setEditable(true);
lv.setCellFactory(
ChoiceBoxListCell.forListView(
bookStringConverter,
dataList.subList(0, 10).toArray(Book[]::new))
);
lv.getItems().setAll(
dataList.stream().limit(10).collect(Collectors.toList())
);
return lv;
}
private ListView<Book> comboBoxList() {
var lv = new ListView<Book>();
lv.setEditable(true);
lv.setCellFactory(
ComboBoxListCell.forListView(
bookStringConverter,
dataList.subList(0, 10).toArray(Book[]::new))
);
lv.getItems().setAll(
dataList.stream().limit(10).collect(Collectors.toList())
);
return lv;
}
private ListView<Book> nestedControlsList() {
var lv = new ListView<Book>();
lv.setCellFactory(book -> new NestedControlsListCell());
lv.getItems().setAll(
dataList.stream().limit(10).collect(Collectors.toList())
);
return lv;
}
private enum Example {
TEXT("Text"),
EDITABLE("TextFieldListCell"),
CHECK_BOX("CheckBoxListCell"),
CHOICE_BOX("ChoiceBoxListCell"),
COMBO_BOX("ComboBoxListCell"),
NESTED_CONTROLS("Nested controls");
private final String name;
Example(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Example find(String name) {
return Arrays.stream(Example.values())
.filter(example -> Objects.equals(example.getName(), name))
.findFirst()
.orElse(null);
}
}
private static class BookStringConverter extends StringConverter<Book> {
private final List<Book> dataList;
public BookStringConverter(List<Book> dataList) {
this.dataList = dataList;
}
@Override
public String toString(Book book) {
if (book == null) {
return null;
}
return String.format("\"%s\" by %s", book.getTitle(), book.getAuthor());
}
@Override
public Book fromString(String s) {
if (s == null) {
return null;
}
int sep = s.indexOf("\" by");
String title = s.substring(1, sep);
String author = s.substring(sep + "\" by".length());
return dataList.stream()
.filter(b -> Objects.equals(b.getTitle(), title)
&& Objects.equals(b.getAuthor(), author)
)
.findFirst()
.orElse(null);
}
}
private static class NestedControlsListCell extends ListCell<Book> {
private final HBox root;
private final Label titleLabel;
private final Hyperlink authorLink;
public NestedControlsListCell() {
titleLabel = new Label();
authorLink = new Hyperlink();
var purchaseBtn = new Button("Purchase");
purchaseBtn.getStyleClass().addAll(Styles.ACCENT);
purchaseBtn.setGraphic(new FontIcon(Feather.SHOPPING_CART));
root = new HBox(5,
titleLabel,
new Label(" by"),
authorLink,
new Spacer(),
purchaseBtn
);
root.setAlignment(Pos.CENTER_LEFT);
}
@Override
public void updateItem(Book book, boolean empty) {
super.updateItem(book, empty);
if (empty) {
setGraphic(null);
return;
}
titleLabel.setText("\"" + book.getTitle() + "\"");
authorLink.setText(book.getAuthor());
setGraphic(root);
}
}
}

@ -0,0 +1,236 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.sampler.page.AbstractPage;
import java.util.stream.IntStream;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import net.datafaker.Faker;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
public class MenuBarPage extends AbstractPage {
public static final String NAME = "MenuBar";
@Override
public String getName() {
return NAME;
}
public MenuBarPage() {
super();
addFormattedText("""
A menu bar is a user interface component that typically appears at the top of \
an application window or screen, and provides a series of drop-down menus that \
allow users to access various features and functions of the application."""
);
addNode(menuBarSample());
}
private ExampleMenuBar menuBarSample() {
return new ExampleMenuBar();
}
///////////////////////////////////////////////////////////////////////////
public static class ExampleMenuBar extends MenuBar {
private static final Faker FAKER = new Faker();
private static final EventHandler<ActionEvent> SYSTEM_OUT = System.out::println;
public ExampleMenuBar() {
getMenus().addAll(
fileMenu(),
editMenu(),
viewMenu(),
toolsMenu(),
aboutMenu()
);
}
private Menu fileMenu() {
var menu = new Menu("_File");
menu.setMnemonicParsing(true);
menu.setOnAction(SYSTEM_OUT);
var newMenu = createItem(
"_New", null, new KeyCodeCombination(KeyCode.N, CONTROL_DOWN)
);
newMenu.setMnemonicParsing(true);
newMenu.setOnAction(SYSTEM_OUT);
var openRecentMenu = new Menu("Open _Recent");
openRecentMenu.setMnemonicParsing(true);
openRecentMenu.setOnAction(SYSTEM_OUT);
openRecentMenu.getItems().addAll(
IntStream.range(0, 10)
.mapToObj(x -> new MenuItem(FAKER.file().fileName()))
.toList()
);
menu.getItems().addAll(
newMenu,
new SeparatorMenuItem(),
createItem(
"Open", Feather.FOLDER, new KeyCodeCombination(KeyCode.O, CONTROL_DOWN)
),
openRecentMenu,
new SeparatorMenuItem(),
createItem(
"Save", Feather.SAVE, new KeyCodeCombination(KeyCode.S, CONTROL_DOWN)
),
new MenuItem("Save As"),
new SeparatorMenuItem(),
new MenuItem("Exit")
);
return menu;
}
private Menu editMenu() {
var menu = new Menu("_Edit");
menu.setMnemonicParsing(true);
menu.setOnAction(SYSTEM_OUT);
menu.getItems().addAll(
createItem(
"Undo", Feather.CORNER_DOWN_LEFT, new KeyCodeCombination(KeyCode.Z, CONTROL_DOWN)
),
createItem(
"Redo", Feather.CORNER_DOWN_RIGHT, new KeyCodeCombination(KeyCode.Y, CONTROL_DOWN)
),
new SeparatorMenuItem(),
createItem(
"Cut", Feather.SCISSORS, new KeyCodeCombination(KeyCode.X, CONTROL_DOWN)
),
createItem(
"Copy", Feather.COPY, new KeyCodeCombination(KeyCode.C, CONTROL_DOWN)
),
createItem(
"Paste", Feather.CORNER_DOWN_LEFT, new KeyCodeCombination(KeyCode.V, CONTROL_DOWN)
)
);
return menu;
}
private Menu viewMenu() {
var menu = new Menu("_View");
menu.setMnemonicParsing(true);
menu.setOnAction(SYSTEM_OUT);
var showToolbarItem = new CheckMenuItem(
"Show Toolbar", new FontIcon(Feather.TOOL)
);
showToolbarItem.setSelected(true);
showToolbarItem.setAccelerator(
new KeyCodeCombination(KeyCode.T, CONTROL_DOWN)
);
var showGridItem = new CheckMenuItem(
"Show Grid", new FontIcon(Feather.GRID));
var captionItem = new CaptionMenuItem("Layout");
var viewToggleGroup = new ToggleGroup();
var toggleItem1 = new RadioMenuItem(
"Single", new FontIcon(Material2OutlinedAL.LOOKS_ONE)
);
toggleItem1.setSelected(true);
toggleItem1.setToggleGroup(viewToggleGroup);
var toggleItem2 = new RadioMenuItem(
"Two Columns", new FontIcon(Material2OutlinedAL.LOOKS_TWO)
);
toggleItem2.setToggleGroup(viewToggleGroup);
var toggleItem3 = new RadioMenuItem(
"Three Columns", new FontIcon(Material2OutlinedAL.LOOKS_3)
);
toggleItem3.setToggleGroup(viewToggleGroup);
menu.getItems().addAll(
showToolbarItem,
showGridItem,
new SeparatorMenuItem(),
captionItem,
toggleItem1,
toggleItem2,
toggleItem3
);
return menu;
}
private Menu toolsMenu() {
var menu = new Menu("_Tools");
menu.setMnemonicParsing(true);
menu.setOnAction(SYSTEM_OUT);
menu.setDisable(true);
return menu;
}
private Menu aboutMenu() {
var menu = new Menu(
"_About", new FontIcon(Feather.HELP_CIRCLE)
);
menu.setMnemonicParsing(true);
menu.setOnAction(SYSTEM_OUT);
var deeplyNestedMenu = new Menu(
"Very...", null,
new Menu("Very...", null,
new Menu("Deeply", null,
new Menu("Nested", null,
new MenuItem("Menu")
))));
// NOTE: this won't be displayed because right container is reserved for submenu indication
deeplyNestedMenu.setAccelerator(
new KeyCodeCombination(KeyCode.DIGIT1, SHIFT_DOWN, CONTROL_DOWN)
);
menu.getItems().addAll(
new MenuItem("Help"),
new MenuItem("About"),
new SeparatorMenuItem(),
deeplyNestedMenu
);
return menu;
}
private MenuItem createItem(@Nullable String text,
@Nullable Ikon graphic,
@Nullable KeyCombination accelerator) {
var item = new MenuItem(text);
if (graphic != null) {
item.setGraphic(new FontIcon(graphic));
}
if (accelerator != null) {
item.setAccelerator(accelerator);
}
return item;
}
}
}

@ -2,19 +2,12 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.BUTTON_OUTLINED;
import static atlantafx.base.theme.Styles.DANGER;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.base.theme.Styles.SUCCESS;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
@ -23,14 +16,12 @@ import javafx.scene.control.SplitMenuButton;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class MenuButtonPage extends AbstractPage { public class MenuButtonPage extends OutlinePage {
public static final String NAME = "MenuButton"; public static final String NAME = "MenuButton";
private static final int PREF_WIDTH = 150;
@Override @Override
public String getName() { public String getName() {
@ -39,91 +30,114 @@ public class MenuButtonPage extends AbstractPage {
public MenuButtonPage() { public MenuButtonPage() {
super(); super();
setUserContent(new VBox(
Page.PAGE_VGAP,
expandingHBox(basicSample(), iconOnlySample()),
expandingHBox(coloredSample(), outlinedSample()),
expandingHBox(popupSideSample(), noArrowSample()),
disabledSample()
)); addFormattedText("""
[i]MenuButton[/i] is a button which, when clicked or pressed, will \
show a context (dropdown) menu.
[i]SplitMenuButton[/i] is a variation of menu button. It is broken into two \
pieces, the [i]action[/i] area and the [i]menu open[/i] area. If the user clicks \
in the action area, the [i]SplitMenuButton[/i] will act similarly to [i]Button[/i], \
while the menu open area of the control will show a context menu if clicked."""
);
addSection("Usage", usageExample());
addSection("Colored", coloredExample());
addSection("Popup Side", popupSideExample());
addSection("Icon Button", iconButtonExample());
addSection("Outlined", outlinedExample());
addSection("No Arrow", noArrowExample());
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
var basicMenuBtn = new MenuButton("_Menu Button"); //snippet_1:start
basicMenuBtn.getItems().setAll(createItems(5)); var normalMenuBtn = new MenuButton("_Menu Button");
basicMenuBtn.setMnemonicParsing(true); normalMenuBtn.getItems().setAll(createItems(5));
normalMenuBtn.setMnemonicParsing(true);
var basicSplitBtn = new SplitMenuButton(createItems(5)); var normalSplitBtn = new SplitMenuButton(createItems(5));
basicSplitBtn.setText("_Split Menu Button"); normalSplitBtn.setText("_Split Menu Button");
basicSplitBtn.setMnemonicParsing(true); normalSplitBtn.setMnemonicParsing(true);
var flatMenuBtn = new MenuButton("Flat"); var flatMenuBtn = new MenuButton("Flat");
flatMenuBtn.getItems().setAll(createItems(5)); flatMenuBtn.getItems().setAll(createItems(5));
flatMenuBtn.getStyleClass().add(FLAT); flatMenuBtn.getStyleClass().add(Styles.FLAT);
var flatSplitBtn = new SplitMenuButton(createItems(5)); var flatSplitBtn = new SplitMenuButton(createItems(5));
flatSplitBtn.setText("Flat"); flatSplitBtn.setText("Flat");
flatSplitBtn.getStyleClass().add(FLAT); flatSplitBtn.getStyleClass().add(Styles.FLAT);
//snippet_1:end
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); var box = new FlowPane(
content.getChildren().addAll(basicMenuBtn, basicSplitBtn, flatMenuBtn, flatSplitBtn); HGAP_20, VGAP_10,
normalMenuBtn, normalSplitBtn, flatMenuBtn, flatSplitBtn
);
return new SampleBlock("Basic", content); var description = BBCodeParser.createFormattedText("""
Just like the [i]Button[/i], [i]MenuButton[/i] comes with four \
CSS variants: normal (default), colored, outlined, and flat (or text). \
To change the appearance of the [i]Button[/i], you set the corresponding \
style classes that work as modifiers."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock coloredSample() { private ExampleBox coloredExample() {
//snippet_2:start
var accentMenuBtn = new MenuButton("_Accent"); var accentMenuBtn = new MenuButton("_Accent");
accentMenuBtn.getItems().setAll(createItems(5)); accentMenuBtn.getItems().setAll(createItems(5));
accentMenuBtn.getStyleClass().add(ACCENT); accentMenuBtn.getStyleClass().add(Styles.ACCENT);
accentMenuBtn.setMnemonicParsing(true); accentMenuBtn.setMnemonicParsing(true);
accentMenuBtn.setPrefWidth(PREF_WIDTH); accentMenuBtn.setPrefWidth(150);
var accentSplitBtn = new SplitMenuButton(createItems(5)); var accentSplitBtn = new SplitMenuButton(createItems(5));
accentSplitBtn.setText("Accent"); accentSplitBtn.setText("Accent");
accentSplitBtn.getStyleClass().add(ACCENT); accentSplitBtn.getStyleClass().add(Styles.ACCENT);
accentSplitBtn.setPrefWidth(PREF_WIDTH); accentSplitBtn.setPrefWidth(150);
var successMenuBtn = new MenuButton("Success"); var successMenuBtn = new MenuButton("Success");
successMenuBtn.getItems().setAll(createItems(5)); successMenuBtn.getItems().setAll(createItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK)); successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successMenuBtn.getStyleClass().add(SUCCESS); successMenuBtn.getStyleClass().add(Styles.SUCCESS);
successMenuBtn.setPrefWidth(PREF_WIDTH); successMenuBtn.setPrefWidth(150);
var successSplitBtn = new SplitMenuButton(createItems(5)); var successSplitBtn = new SplitMenuButton(createItems(5));
successSplitBtn.setGraphic(new FontIcon(Feather.CHECK)); successSplitBtn.setGraphic(new FontIcon(Feather.CHECK));
successSplitBtn.setText("_Success"); successSplitBtn.setText("_Success");
successSplitBtn.getStyleClass().add(SUCCESS); successSplitBtn.getStyleClass().add(Styles.SUCCESS);
successSplitBtn.setMnemonicParsing(true); successSplitBtn.setMnemonicParsing(true);
successSplitBtn.setPrefWidth(PREF_WIDTH); successSplitBtn.setPrefWidth(150);
var dangerMenuBtn = new MenuButton("Danger"); var dangerMenuBtn = new MenuButton("Danger");
dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH)); dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerMenuBtn.getItems().setAll(createItems(5)); dangerMenuBtn.getItems().setAll(createItems(5));
dangerMenuBtn.getStyleClass().add(DANGER); dangerMenuBtn.getStyleClass().add(Styles.DANGER);
dangerMenuBtn.setPrefWidth(PREF_WIDTH); dangerMenuBtn.setPrefWidth(150);
var dangerSplitBtn = new SplitMenuButton(createItems(5)); var dangerSplitBtn = new SplitMenuButton(createItems(5));
dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH)); dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerSplitBtn.setText("_Danger"); dangerSplitBtn.setText("_Danger");
dangerSplitBtn.getStyleClass().add(DANGER); dangerSplitBtn.getStyleClass().add(Styles.DANGER);
dangerSplitBtn.setMnemonicParsing(true); dangerSplitBtn.setMnemonicParsing(true);
dangerSplitBtn.setPrefWidth(PREF_WIDTH); dangerSplitBtn.setPrefWidth(150);
//snippet_2:end
var content = new GridPane(); var grid = new GridPane();
content.setHgap(BLOCK_HGAP); grid.setHgap(HGAP_20);
content.setVgap(BLOCK_VGAP); grid.setVgap(VGAP_10);
content.add(accentMenuBtn, 0, 0); grid.addColumn(0, accentMenuBtn, successMenuBtn, dangerMenuBtn);
content.add(accentSplitBtn, 1, 0); grid.addColumn(1, accentSplitBtn, successSplitBtn, dangerSplitBtn);
content.add(successMenuBtn, 0, 1);
content.add(successSplitBtn, 1, 1);
content.add(dangerMenuBtn, 0, 2);
content.add(dangerSplitBtn, 1, 2);
return new SampleBlock("Colored", content); var description = BBCodeParser.createFormattedText("""
You can change the [i]MenuButton[/i] color simply by using predefined \
style class modifiers."""
);
return new ExampleBox(grid, new Snippet(getClass(), 2), description);
} }
private SampleBlock popupSideSample() { private ExampleBox popupSideExample() {
//snippet_3:start
var topMenuBtn = new MenuButton("Top"); var topMenuBtn = new MenuButton("Top");
topMenuBtn.getItems().setAll(createItems(5)); topMenuBtn.getItems().setAll(createItems(5));
topMenuBtn.setPopupSide(Side.TOP); topMenuBtn.setPopupSide(Side.TOP);
@ -131,145 +145,165 @@ public class MenuButtonPage extends AbstractPage {
var rightMenuBtn = new MenuButton("Right"); var rightMenuBtn = new MenuButton("Right");
rightMenuBtn.getItems().setAll(createItems(5)); rightMenuBtn.getItems().setAll(createItems(5));
rightMenuBtn.setPopupSide(Side.RIGHT); rightMenuBtn.setPopupSide(Side.RIGHT);
rightMenuBtn.getStyleClass().add(ACCENT); rightMenuBtn.getStyleClass().add(Styles.ACCENT);
var bottomMenuBtn = new MenuButton("Bottom"); var bottomMenuBtn = new MenuButton("Bottom");
bottomMenuBtn.getItems().setAll(createItems(5)); bottomMenuBtn.getItems().setAll(createItems(5));
bottomMenuBtn.setPopupSide(Side.BOTTOM); bottomMenuBtn.setPopupSide(Side.BOTTOM);
bottomMenuBtn.getStyleClass().add(SUCCESS); bottomMenuBtn.getStyleClass().add(Styles.SUCCESS);
var leftMenuBtn = new MenuButton("Left"); var leftMenuBtn = new MenuButton("Left");
leftMenuBtn.getItems().setAll(createItems(5)); leftMenuBtn.getItems().setAll(createItems(5));
leftMenuBtn.setPopupSide(Side.LEFT); leftMenuBtn.setPopupSide(Side.LEFT);
leftMenuBtn.getStyleClass().add(DANGER); leftMenuBtn.getStyleClass().add(Styles.DANGER);
//snippet_3:end
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); var box = new FlowPane(
content.getChildren().addAll(topMenuBtn, rightMenuBtn, bottomMenuBtn, leftMenuBtn); HGAP_20, VGAP_10,
topMenuBtn, rightMenuBtn, bottomMenuBtn, leftMenuBtn
);
var description = BBCodeParser.createFormattedText("""
A [i]MenuButton[/i] can be set to show its menu on any side of the button. \
This is specified using the [code]popupSide[/code] property. By default the \
menu appears below the button. However, regardless of the popupSide specified, \
if there is not enough room, the [i]ContextMenu[/i] will be smartly repositioned, \
most probably to be on the opposite side of the [i]MenuButton[/i].""");
return new SampleBlock("Popup Side", content); return new ExampleBox(box, new Snippet(getClass(), 3), description);
} }
private SampleBlock iconOnlySample() { private ExampleBox iconButtonExample() {
var basicMenuBtn = new MenuButton(); //snippet_4:start
basicMenuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL)); var normalMenuBtn = new MenuButton();
basicMenuBtn.getItems().setAll(createItems(5)); normalMenuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicMenuBtn.getStyleClass().addAll(BUTTON_ICON); normalMenuBtn.getItems().setAll(createItems(5));
normalMenuBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
var basicSplitBtn = new SplitMenuButton(createItems(5)); var normalSplitBtn = new SplitMenuButton(createItems(5));
basicSplitBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL)); normalSplitBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicSplitBtn.getStyleClass().addAll(BUTTON_ICON); normalSplitBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
var accentMenuBtn = new MenuButton(); var accentMenuBtn = new MenuButton();
accentMenuBtn.setGraphic(new FontIcon(Feather.MENU)); accentMenuBtn.setGraphic(new FontIcon(Feather.MENU));
accentMenuBtn.getItems().setAll(createItems(5)); accentMenuBtn.getItems().setAll(createItems(5));
accentMenuBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT); accentMenuBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.ACCENT
);
var accentSplitBtn = new SplitMenuButton(createItems(5)); var accentSplitBtn = new SplitMenuButton(createItems(5));
accentSplitBtn.setGraphic(new FontIcon(Feather.MENU)); accentSplitBtn.setGraphic(new FontIcon(Feather.MENU));
accentSplitBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT); accentSplitBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.ACCENT
);
//snippet_4:end
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP); var box = new FlowPane(
content.getChildren().addAll(basicMenuBtn, basicSplitBtn, accentMenuBtn, accentSplitBtn); HGAP_20, VGAP_10,
normalMenuBtn, normalSplitBtn, accentMenuBtn, accentSplitBtn
);
var description = BBCodeParser.createFormattedText("""
You can hide the [i]MenuButton[/i] text by applying [code]Styles.BUTTON_ICON[/code] \
style class modifier."""
);
return new SampleBlock("Icons", content); return new ExampleBox(box, new Snippet(getClass(), 4), description);
} }
private SampleBlock outlinedSample() { private ExampleBox outlinedExample() {
//snippet_5:start
var accentMenuBtn = new MenuButton("Accen_t"); var accentMenuBtn = new MenuButton("Accen_t");
accentMenuBtn.getItems().setAll(createItems(5)); accentMenuBtn.getItems().setAll(createItems(5));
accentMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT); accentMenuBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.ACCENT
);
accentMenuBtn.setMnemonicParsing(true); accentMenuBtn.setMnemonicParsing(true);
accentMenuBtn.setPrefWidth(PREF_WIDTH); accentMenuBtn.setPrefWidth(150);
var accentSplitBtn = new SplitMenuButton(createItems(5)); var accentSplitBtn = new SplitMenuButton(createItems(5));
accentSplitBtn.setText("Accent"); accentSplitBtn.setText("Accent");
accentSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT); accentSplitBtn.getStyleClass().addAll(
accentSplitBtn.setPrefWidth(PREF_WIDTH); Styles.BUTTON_OUTLINED, Styles.ACCENT
);
accentSplitBtn.setPrefWidth(150);
var successMenuBtn = new MenuButton("S_uccess"); var successMenuBtn = new MenuButton("S_uccess");
successMenuBtn.getItems().setAll(createItems(5)); successMenuBtn.getItems().setAll(createItems(5));
successMenuBtn.setGraphic(new FontIcon(Feather.CHECK)); successMenuBtn.setGraphic(new FontIcon(Feather.CHECK));
successMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS); successMenuBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.SUCCESS
);
successMenuBtn.setMnemonicParsing(true); successMenuBtn.setMnemonicParsing(true);
successMenuBtn.setPrefWidth(PREF_WIDTH); successMenuBtn.setPrefWidth(150);
var successSplitBtn = new SplitMenuButton(createItems(5)); var successSplitBtn = new SplitMenuButton(createItems(5));
successSplitBtn.setGraphic(new FontIcon(Feather.CHECK)); successSplitBtn.setGraphic(new FontIcon(Feather.CHECK));
successSplitBtn.setText("Success"); successSplitBtn.setText("Success");
successSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, SUCCESS); successSplitBtn.getStyleClass().addAll(
successSplitBtn.setPrefWidth(PREF_WIDTH); Styles.BUTTON_OUTLINED, Styles.SUCCESS
);
successSplitBtn.setPrefWidth(150);
var dangerMenuBtn = new MenuButton("Danger"); var dangerMenuBtn = new MenuButton("Danger");
dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH)); dangerMenuBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerMenuBtn.getItems().setAll(createItems(5)); dangerMenuBtn.getItems().setAll(createItems(5));
dangerMenuBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER); dangerMenuBtn.getStyleClass().addAll(
dangerMenuBtn.setPrefWidth(PREF_WIDTH); Styles.BUTTON_OUTLINED, Styles.DANGER
);
dangerMenuBtn.setPrefWidth(150);
var dangerSplitBtn = new SplitMenuButton(createItems(5)); var dangerSplitBtn = new SplitMenuButton(createItems(5));
dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH)); dangerSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
dangerSplitBtn.setText("Dan_ger"); dangerSplitBtn.setText("Dan_ger");
dangerSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER); dangerSplitBtn.getStyleClass().addAll(
Styles.BUTTON_OUTLINED, Styles.DANGER
);
dangerSplitBtn.setMnemonicParsing(true); dangerSplitBtn.setMnemonicParsing(true);
dangerSplitBtn.setPrefWidth(PREF_WIDTH); dangerSplitBtn.setPrefWidth(150);
//snippet_5:end
var content = new GridPane(); var grid = new GridPane();
content.setHgap(BLOCK_HGAP); grid.setHgap(HGAP_20);
content.setVgap(BLOCK_VGAP); grid.setVgap(VGAP_10);
content.add(accentMenuBtn, 0, 0); grid.addColumn(0, accentMenuBtn, successMenuBtn, dangerMenuBtn);
content.add(accentSplitBtn, 1, 0); grid.addColumn(1, accentSplitBtn, successSplitBtn, dangerSplitBtn);
content.add(successMenuBtn, 0, 1);
content.add(successSplitBtn, 1, 1);
content.add(dangerMenuBtn, 0, 2);
content.add(dangerSplitBtn, 1, 2);
return new SampleBlock("Outlined", content); var description = BBCodeParser.createFormattedText("""
Outlined buttons are medium-emphasis buttons. They contain actions that are \
important but aren't the primary action in an app."""
);
return new ExampleBox(grid, new Snippet(getClass(), 5), description);
} }
private SampleBlock disabledSample() { private ExampleBox noArrowExample() {
var basicMenuBtn = new MenuButton("Menu Button"); //snippet_6:start
basicMenuBtn.getItems().setAll(createItems(5)); var normalMenuBtn = new MenuButton("_Menu Button");
basicMenuBtn.setDisable(true); normalMenuBtn.getItems().setAll(createItems(5));
normalMenuBtn.getStyleClass().addAll(Tweaks.NO_ARROW);
var accentSplitBtn = new SplitMenuButton();
accentSplitBtn.setText("Accent");
accentSplitBtn.getItems().setAll(createItems(5));
accentSplitBtn.getStyleClass().addAll(ACCENT);
accentSplitBtn.setDisable(true);
var flatMenuBtn = new MenuButton("Flat"); var flatMenuBtn = new MenuButton("Flat");
flatMenuBtn.getItems().setAll(createItems(5)); flatMenuBtn.getItems().setAll(createItems(5));
flatMenuBtn.getStyleClass().addAll(FLAT); flatMenuBtn.getStyleClass().addAll(
flatMenuBtn.setDisable(true); Styles.FLAT, Tweaks.NO_ARROW
);
var iconMenuBtn = new MenuButton();
iconMenuBtn.getItems().setAll(createItems(5));
iconMenuBtn.getStyleClass().addAll(BUTTON_ICON);
iconMenuBtn.setDisable(true);
var sample = new HBox(BLOCK_HGAP);
sample.getChildren().addAll(basicMenuBtn, accentSplitBtn, flatMenuBtn, iconMenuBtn);
return new SampleBlock("Disabled", sample);
}
private SampleBlock noArrowSample() {
var basicMenuBtn = new MenuButton("_Menu Button");
basicMenuBtn.getItems().setAll(createItems(5));
basicMenuBtn.getStyleClass().addAll(Tweaks.NO_ARROW);
var flatMenuBtn = new MenuButton("Flat");
flatMenuBtn.getItems().setAll(createItems(5));
flatMenuBtn.getStyleClass().addAll(FLAT, Tweaks.NO_ARROW);
var iconMenuBtn = new MenuButton(); var iconMenuBtn = new MenuButton();
iconMenuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL)); iconMenuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
iconMenuBtn.getItems().setAll(createItems(5)); iconMenuBtn.getItems().setAll(createItems(5));
iconMenuBtn.getStyleClass().addAll(BUTTON_ICON, Tweaks.NO_ARROW); iconMenuBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Tweaks.NO_ARROW
);
//snippet_6:end
var content = new HBox(BLOCK_HGAP); var box = new HBox(HGAP_20);
content.getChildren().addAll(basicMenuBtn, flatMenuBtn, iconMenuBtn); box.getChildren().addAll(normalMenuBtn, flatMenuBtn, iconMenuBtn);
return new SampleBlock("No Arrow", content); var description = BBCodeParser.createFormattedText("""
To hide the [i]MenuButton[/i] arrow, use the [code]Tweak.NO_ARROW[/code] \
style class modifier."""
);
return new ExampleBox(box, new Snippet(getClass(), 6), description);
} }
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")

@ -1,72 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.fake.SampleMenuBar;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.util.Controls;
import javafx.geometry.Pos;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
public class MenuPage extends AbstractPage {
public static final String NAME = "Menu";
@Override
public String getName() {
return NAME;
}
public MenuPage() {
super();
setUserContent(new VBox(Page.PAGE_VGAP,
menuBarSample(),
contextMenuExample()
));
}
private SampleBlock menuBarSample() {
return new SampleBlock("Menu Bar", new SampleMenuBar(FAKER));
}
private SampleBlock contextMenuExample() {
var contextMenu = new ContextMenu();
var undoItem =
Controls.menuItem("_Undo", Feather.CORNER_DOWN_LEFT, new KeyCodeCombination(KeyCode.Z, CONTROL_DOWN));
undoItem.setMnemonicParsing(true);
var redoItem =
Controls.menuItem("_Redo", Feather.CORNER_DOWN_RIGHT, new KeyCodeCombination(KeyCode.Y, CONTROL_DOWN));
redoItem.setMnemonicParsing(true);
contextMenu.getItems().addAll(
undoItem,
redoItem,
new SeparatorMenuItem(),
Controls.menuItem("Cut", Feather.SCISSORS, new KeyCodeCombination(KeyCode.X, CONTROL_DOWN)),
Controls.menuItem("Copy", Feather.COPY, new KeyCodeCombination(KeyCode.C, CONTROL_DOWN)),
Controls.menuItem("Paste", null, new KeyCodeCombination(KeyCode.V, CONTROL_DOWN))
);
var content = new Label("Right-Click Here");
content.setAlignment(Pos.CENTER);
content.setMinSize(400, 80);
content.setMaxSize(400, 80);
content.setContextMenu(contextMenu);
content.getStyleClass().add(Styles.BORDERED);
return new SampleBlock("Context Menu", content);
}
}

@ -1,239 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ModalPane;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
public class ModalPanePage extends AbstractPage {
public static final String NAME = "Modal Pane";
private final ModalPane modalPaneL1 = new ModalPane();
private final ModalPane modalPaneL2 = new ModalPane(-15);
private final ModalPane modalPaneL3 = new ModalPane(-20);
private VBox centerDialog;
private VBox topDialog;
private VBox rightDialog;
private VBox bottomDialog;
private VBox leftDialog;
@Override
public String getName() {
return NAME;
}
public ModalPanePage() {
super();
userContent.getChildren().setAll(
modalPaneL1,
modalPaneL2,
modalPaneL3,
new SampleBlock("Playground", createPlayground())
);
}
private VBox createPlayground() {
var controlPane = new GridPane();
controlPane.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
controlPane.setMaxSize(300, 300);
controlPane.setHgap(20);
controlPane.setVgap(20);
controlPane.getRowConstraints().addAll(
new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false),
new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false),
new RowConstraints(50, 50, 50, Priority.NEVER, VPos.CENTER, false)
);
var topBtn = new Button("Top");
topBtn.setOnAction(e -> {
modalPaneL1.setAlignment(Pos.TOP_CENTER);
modalPaneL1.usePredefinedTransitionFactories(Side.TOP);
modalPaneL1.show(getOrCreateTopDialog());
});
controlPane.add(topBtn, 1, 0);
var rightBtn = new Button("Right");
rightBtn.setOnAction(e -> {
modalPaneL1.setAlignment(Pos.TOP_RIGHT);
modalPaneL1.usePredefinedTransitionFactories(Side.RIGHT);
modalPaneL1.show(getOrCreateRightDialog());
});
controlPane.add(rightBtn, 2, 1);
var centerBtn = new Button("Center");
centerBtn.setOnAction(e -> {
modalPaneL1.setAlignment(Pos.CENTER);
modalPaneL1.usePredefinedTransitionFactories(null);
modalPaneL1.show(getOrCreateCenterDialog());
});
controlPane.add(centerBtn, 1, 1);
var bottomBtn = new Button("Bottom");
bottomBtn.setOnAction(e -> {
modalPaneL1.setAlignment(Pos.BOTTOM_CENTER);
modalPaneL1.usePredefinedTransitionFactories(Side.BOTTOM);
modalPaneL1.show(getOrCreateBottomDialog());
});
controlPane.add(bottomBtn, 1, 2);
var leftBtn = new Button("Left");
leftBtn.setOnAction(e -> {
modalPaneL1.setAlignment(Pos.TOP_LEFT);
modalPaneL1.usePredefinedTransitionFactories(Side.LEFT);
modalPaneL1.show(getOrCreateLeftDialog());
});
controlPane.add(leftBtn, 0, 1);
controlPane.getChildren().forEach(c -> ((Button) c).setPrefWidth(100));
var root = new VBox(controlPane);
root.setAlignment(Pos.CENTER);
return root;
}
private Pane getOrCreateCenterDialog() {
if (centerDialog != null) {
return centerDialog;
}
centerDialog = createGenericDialog(450, 450, e -> modalPaneL1.hide(true));
return centerDialog;
}
private Node getOrCreateTopDialog() {
if (topDialog != null) {
return topDialog;
}
topDialog = createGenericDialog(-1, 150, e -> modalPaneL1.hide(true));
return topDialog;
}
private Node getOrCreateRightDialog() {
if (rightDialog != null) {
return rightDialog;
}
rightDialog = createGenericDialog(250, -1, e -> modalPaneL1.hide(true));
return rightDialog;
}
private Node getOrCreateBottomDialog() {
if (bottomDialog != null) {
return bottomDialog;
}
bottomDialog = createGenericDialog(-1, 150, e -> modalPaneL1.hide(true));
return bottomDialog;
}
private Node getOrCreateLeftDialog() {
if (leftDialog != null) {
return leftDialog;
}
leftDialog = createGenericDialog(250, -1, e -> modalPaneL1.hide(true));
return leftDialog;
}
private VBox createOverflowDialog() {
var dialog = createGenericDialog(400, 400, e1 -> modalPaneL1.hide(true));
var content = new VBox();
for (int i = 0; i < 10; i++) {
var r = new Rectangle(600, 100);
if (i % 2 == 0) {
r.setFill(Color.AZURE);
} else {
r.setFill(Color.TOMATO);
}
content.getChildren().add(r);
}
var scrollPane = new ScrollPane();
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setFitToHeight(true);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setFitToWidth(true);
scrollPane.setMaxHeight(20_000);
scrollPane.setContent(content);
dialog.getChildren().setAll(scrollPane);
return dialog;
}
private VBox createFullScreenDialog() {
return createGenericDialog(-1, -1, e1 -> modalPaneL1.hide(true));
}
private VBox createLevel1Dialog() {
var dialog = createGenericDialog(600, 600, e1 -> modalPaneL1.hide(true));
var nextDialogBtn = new Button("Dialog 2");
nextDialogBtn.setOnAction(e -> {
modalPaneL2.setAlignment(Pos.CENTER);
modalPaneL2.usePredefinedTransitionFactories(null);
modalPaneL2.show(createLevel2Dialog());
});
dialog.getChildren().add(nextDialogBtn);
return dialog;
}
private VBox createLevel2Dialog() {
var dialog = createGenericDialog(450, 450, e2 -> modalPaneL2.hide(true));
var nextDialogBtn = new Button("Dialog 3");
nextDialogBtn.setOnAction(e -> {
modalPaneL3.setAlignment(Pos.CENTER);
modalPaneL3.usePredefinedTransitionFactories(null);
modalPaneL3.show(createLevel3Dialog());
});
dialog.getChildren().add(nextDialogBtn);
return dialog;
}
private VBox createLevel3Dialog() {
return createGenericDialog(300, 300, e2 -> modalPaneL3.hide(true));
}
private VBox createGenericDialog(double width, double height, EventHandler<ActionEvent> closeHandler) {
var dialog = new VBox(10);
dialog.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
dialog.setAlignment(Pos.CENTER);
dialog.setMinSize(width, height);
dialog.setMaxSize(width, height);
var closeBtn = new Button("Close");
closeBtn.setOnAction(closeHandler);
dialog.getChildren().add(closeBtn);
return dialog;
}
}

@ -1,326 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT;
import static atlantafx.base.theme.Styles.BUTTON_CIRCLE;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.BUTTON_OUTLINED;
import static atlantafx.base.theme.Styles.DANGER;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.base.theme.Styles.LEFT_PILL;
import static atlantafx.base.theme.Styles.RIGHT_PILL;
import static atlantafx.base.theme.Styles.SUCCESS;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.IntegerStringConverter;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.stream.IntStream;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.PasswordField;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Slider;
import javafx.scene.control.Spinner;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class OverviewPage extends AbstractPage {
public static final String NAME = "Overview";
private static final int BUTTON_WIDTH = 120;
private static final int COMBO_BOX_WIDTH = 150;
@Override
public String getName() {
return NAME;
}
public OverviewPage() {
super();
setUserContent(new VBox(
PAGE_VGAP,
buttonSample(),
expandingHBox(iconButtonSample(), dropdownMenuSample()),
expandingHBox(checkBoxSample(), radioButtonSample(), toggleSwitchSample()),
comboBoxSample(),
sliderSample(),
expandingHBox(textFieldSample(), spinnerSample()),
textAreaSample()
));
}
private SampleBlock buttonSample() {
var basicBtn = new Button("Basic");
basicBtn.setPrefWidth(BUTTON_WIDTH);
var defaultBtn = new Button("Default");
defaultBtn.setDefaultButton(true);
defaultBtn.setPrefWidth(BUTTON_WIDTH);
var successBtn = new Button("Success");
successBtn.getStyleClass().add(SUCCESS);
successBtn.setPrefWidth(BUTTON_WIDTH);
var dangerBtn = new Button("Danger");
dangerBtn.getStyleClass().add(DANGER);
dangerBtn.setPrefWidth(BUTTON_WIDTH);
var flatBtn = new Button("Flat");
flatBtn.getStyleClass().add(FLAT);
flatBtn.setPrefWidth(BUTTON_WIDTH);
var outlinedBtn = new Button("Outlined");
outlinedBtn.getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
outlinedBtn.setPrefWidth(BUTTON_WIDTH);
var twoButtonGroup = new ToggleGroup();
var leftPill = createToggleButton("Toggle 1", twoButtonGroup, true, LEFT_PILL);
leftPill.setPrefWidth(BUTTON_WIDTH + BLOCK_HGAP / 2.0);
var rightPill = createToggleButton("Toggle 2", twoButtonGroup, false, RIGHT_PILL);
rightPill.setPrefWidth(BUTTON_WIDTH + BLOCK_HGAP / 2.0);
var twoButtonBox = new HBox(leftPill, rightPill);
var grid = new GridPane();
grid.setVgap(BLOCK_VGAP);
grid.setHgap(BLOCK_HGAP);
grid.add(basicBtn, 0, 0);
grid.add(flatBtn, 1, 0);
grid.add(successBtn, 2, 0);
grid.add(dangerBtn, 3, 0);
grid.add(defaultBtn, 0, 1);
grid.add(outlinedBtn, 1, 1);
grid.add(twoButtonBox, 2, 1, 2, 1);
return new SampleBlock("Buttons", grid);
}
private SampleBlock iconButtonSample() {
var basicBtn = new Button("", new FontIcon(Feather.MORE_HORIZONTAL));
basicBtn.getStyleClass().addAll(BUTTON_ICON);
var successBtn = new Button("", new FontIcon(Feather.PLUS));
successBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var dangerBtn = new Button("", new FontIcon(Feather.TRASH));
dangerBtn.getStyleClass().addAll(BUTTON_ICON, DANGER);
var basicCircularBtn = new Button("", new FontIcon(Feather.MORE_VERTICAL));
basicCircularBtn.getStyleClass().addAll(BUTTON_CIRCLE);
basicCircularBtn.setShape(new Circle(50));
var flatBtn = new Button("", new FontIcon(Feather.MIC));
flatBtn.getStyleClass().addAll(BUTTON_CIRCLE, FLAT);
flatBtn.setShape(new Circle(50));
var grid = new GridPane();
grid.setVgap(BLOCK_VGAP);
grid.setHgap(BLOCK_HGAP);
grid.add(basicBtn, 0, 0);
grid.add(successBtn, 1, 0);
grid.add(dangerBtn, 2, 0);
grid.add(basicCircularBtn, 0, 1);
grid.add(flatBtn, 1, 1);
return new SampleBlock("Icon Buttons", grid);
}
private SampleBlock dropdownMenuSample() {
var basicMenuBtn = new MenuButton("Menu Button");
basicMenuBtn.getItems().setAll(createMenuItems());
basicMenuBtn.setPrefWidth(COMBO_BOX_WIDTH);
var basicIconBtn = new MenuButton();
basicIconBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
basicIconBtn.getItems().setAll(createMenuItems());
basicIconBtn.getStyleClass().addAll(BUTTON_ICON);
var accentIconBtn = new MenuButton();
accentIconBtn.setGraphic(new FontIcon(Feather.MENU));
accentIconBtn.getItems().setAll(createMenuItems());
accentIconBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT);
var basicSplitBtn = new SplitMenuButton(createMenuItems());
basicSplitBtn.setText("Split Menu Button");
var outlinedSplitBtn = new SplitMenuButton(createMenuItems());
outlinedSplitBtn.setGraphic(new FontIcon(Feather.TRASH));
outlinedSplitBtn.setText("Danger");
outlinedSplitBtn.getStyleClass().addAll(BUTTON_OUTLINED, DANGER);
outlinedSplitBtn.setPrefWidth(COMBO_BOX_WIDTH);
var grid = new GridPane();
grid.setVgap(BLOCK_VGAP);
grid.setHgap(BLOCK_HGAP);
grid.add(basicMenuBtn, 0, 0);
grid.add(basicIconBtn, 1, 0);
grid.add(accentIconBtn, 2, 0);
grid.add(basicSplitBtn, 1, 1, 2, 1);
grid.add(outlinedSplitBtn, 0, 1);
return new SampleBlock("Dropdown Menus", grid);
}
private SampleBlock checkBoxSample() {
var opt1 = new CheckBox("Option 1");
var opt2 = new CheckBox("Option 2");
opt2.setSelected(true);
var opt3 = new CheckBox("Option 3");
opt3.setAllowIndeterminate(true);
opt3.setIndeterminate(true);
var container = new VBox(BLOCK_VGAP, opt1, opt2, opt3);
return new SampleBlock("Check Boxes", container);
}
private SampleBlock radioButtonSample() {
var group = new ToggleGroup();
var opt1 = new RadioButton("Option 1");
opt1.setToggleGroup(group);
var opt2 = new RadioButton("Option 2");
opt2.setToggleGroup(group);
opt2.setSelected(true);
var opt3 = new RadioButton("Option 3");
opt3.setToggleGroup(group);
var container = new VBox(BLOCK_VGAP, opt1, opt2, opt3);
return new SampleBlock("Radio Buttons", container);
}
private SampleBlock toggleSwitchSample() {
var switch1 = new ToggleSwitch();
var switch2 = new ToggleSwitch();
switch2.setSelected(true);
var container = new VBox(BLOCK_VGAP, switch1, switch2);
return new SampleBlock("Switches", container);
}
private SampleBlock comboBoxSample() {
var comboBox = new ComboBox<String>();
comboBox.getItems().setAll("Option 1", "Option 2", "Option 3");
comboBox.getStyleClass().add(Tweaks.ALT_ICON);
comboBox.getSelectionModel().selectFirst();
comboBox.setPrefWidth(COMBO_BOX_WIDTH);
var choiceBox = new ChoiceBox<String>();
choiceBox.getItems().setAll("Option 1", "Option 2", "Option 3");
choiceBox.getSelectionModel().selectFirst();
choiceBox.setPrefWidth(COMBO_BOX_WIDTH);
var datePicker = new DatePicker();
datePicker.setPrefWidth(COMBO_BOX_WIDTH);
datePicker.setValue(LocalDate.now(ZoneId.systemDefault()));
var colorPicker = new ColorPicker();
colorPicker.setPrefWidth(COMBO_BOX_WIDTH);
colorPicker.setValue(Color.ORANGE);
var container = new HBox(BLOCK_HGAP, comboBox, choiceBox, datePicker, colorPicker);
return new SampleBlock("Combo Boxes", container);
}
private SampleBlock sliderSample() {
var slider = new Slider(1, 5, 3);
slider.setPrefWidth(BUTTON_WIDTH * 2);
var tickSlider = new Slider(0, 5, 3);
tickSlider.setShowTickLabels(true);
tickSlider.setShowTickMarks(true);
tickSlider.setMajorTickUnit(1);
tickSlider.setBlockIncrement(1);
tickSlider.setMinorTickCount(5);
tickSlider.setSnapToTicks(true);
tickSlider.setPrefWidth(BUTTON_WIDTH * 2);
tickSlider.setSkin(new ProgressSliderSkin(tickSlider));
var container = new HBox(BLOCK_HGAP, slider, tickSlider);
return new SampleBlock("Sliders", container);
}
private SampleBlock textFieldSample() {
var textField = new TextField("Text");
textField.setPrefWidth(COMBO_BOX_WIDTH);
var passwordField = new PasswordField();
passwordField.setText(FAKER.internet().password());
passwordField.setPrefWidth(COMBO_BOX_WIDTH);
var container = new HBox(BLOCK_HGAP, textField, passwordField);
return new SampleBlock("Text Fields", container);
}
private SampleBlock spinnerSample() {
var spinner1 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner1);
spinner1.setPrefWidth(COMBO_BOX_WIDTH);
var spinner2 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner2);
spinner2.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
spinner2.setPrefWidth(COMBO_BOX_WIDTH);
var container = new HBox(BLOCK_HGAP, spinner1, spinner2);
return new SampleBlock("Spinners", container);
}
private SampleBlock textAreaSample() {
var textArea = new TextArea(String.join("\n\n", FAKER.lorem().paragraphs(3)));
textArea.setWrapText(true);
textArea.setMaxWidth(Double.MAX_VALUE);
textArea.setMinHeight(100);
return new SampleBlock("Text Area", textArea);
}
private ToggleButton createToggleButton(String text,
ToggleGroup group,
boolean selected,
String... styleClasses) {
var toggleButton = new ToggleButton(text);
if (group != null) {
toggleButton.setToggleGroup(group);
}
toggleButton.setSelected(selected);
toggleButton.getStyleClass().addAll(styleClasses);
return toggleButton;
}
private MenuItem[] createMenuItems() {
return IntStream.range(0, 5)
.mapToObj(i -> new MenuItem(FAKER.babylon5().character()))
.toArray(MenuItem[]::new);
}
}

@ -2,13 +2,12 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Pagination; import javafx.scene.control.Pagination;
@ -16,12 +15,12 @@ import javafx.scene.control.Separator;
import javafx.scene.control.Spinner; import javafx.scene.control.Spinner;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
public class PaginationPage extends AbstractPage { public class PaginationPage extends OutlinePage {
public static final String NAME = "Pagination"; public static final String NAME = "Pagination";
private static final int PREF_CONTROL_WIDTH = 120;
@Override @Override
public String getName() { public String getName() {
@ -30,43 +29,113 @@ public class PaginationPage extends AbstractPage {
public PaginationPage() { public PaginationPage() {
super(); super();
setUserContent(new VBox(
new SampleBlock("Playground", createPlayground()) addFormattedText("""
)); A [i]Pagination[/i] control is used for navigation between pages of a single content, \
which has been divided into smaller parts."""
);
addSection("Usage", usageExample());
addSection("Bullet Style", bulletStyleExample());
addSection("No Arrows", noArrowsExample());
addSection("Playground", playground());
} }
private VBox createPlayground() { private ExampleBox usageExample() {
var pagination = new Pagination(); //snippet_1:start
pagination.setCurrentPageIndex(1); var pg = new Pagination(25, 0);
pagination.setPageFactory(index -> { pg.setMaxPageIndicatorCount(5);
pg.setPageFactory(index -> {
var label = new Label("Page #" + (index + 1));
label.setStyle("-fx-font-size: 2em;");
return new BorderPane(label);
});
//snippet_1:end
var box = new HBox(pg);
var description = BBCodeParser.createFormattedText("""
A pagination control consists of the page content and the page navigation areas. \
The [i]Page[/i] content area renders and lays out the content according to the \
application logic. The [i]Page[/i] navigation area contains a prefabricated control \
to preview a particular part of the content."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox bulletStyleExample() {
//snippet_2:start
var pg = new Pagination(25, 0);
pg.setMaxPageIndicatorCount(5);
pg.getStyleClass().add(Pagination.STYLE_CLASS_BULLET);
pg.setPageFactory(index -> {
var label = new Label("Page #" + (index + 1));
label.setStyle("-fx-font-size: 2em;");
return new BorderPane(label);
});
//snippet_2:end
var box = new HBox(pg);
var description = BBCodeParser.createFormattedText("""
The control can be customized to display bullet style indicators by setting \
the style class [code]STYLE_CLASS_BULLET[/code]."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox noArrowsExample() {
//snippet_3:start
var pg = new Pagination(25, 0);
pg.setMaxPageIndicatorCount(5);
pg.setStyle("-fx-arrows-visible:false;");
pg.setPageFactory(index -> {
var label = new Label("Page #" + (index + 1));
label.setStyle("-fx-font-size: 2em;");
return new BorderPane(label);
});
//snippet_3:end
var box = new HBox(pg);
var description = BBCodeParser.createFormattedText("""
[code]-fx-arrows-visible[/code] can be used to to toggle the visibility of the [i]Next[/i] \
and [i]Previous[/i] button arrows."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private VBox playground() {
var pg = new Pagination();
pg.setCurrentPageIndex(0);
pg.setPageFactory(index -> {
var label = new Label("Page #" + (index + 1)); var label = new Label("Page #" + (index + 1));
label.setStyle("-fx-font-size: 3em;"); label.setStyle("-fx-font-size: 3em;");
return new BorderPane(label);
var page = new BorderPane();
page.setCenter(label);
return page;
}); });
// == CONTROLS == // == CONTROLS ==
var pageCountSpinner = new Spinner<Integer>(0, 50, 25); var pageCountSpinner = new Spinner<Integer>(0, 50, 25);
pageCountSpinner.setPrefWidth(PREF_CONTROL_WIDTH); pageCountSpinner.setPrefWidth(120);
pagination.pageCountProperty().bind(pageCountSpinner.valueProperty()); pg.pageCountProperty().bind(pageCountSpinner.valueProperty());
var visibleCountSpinner = new Spinner<Integer>(3, 10, 5); var visibleCountSpinner = new Spinner<Integer>(3, 10, 5);
visibleCountSpinner.setPrefWidth(PREF_CONTROL_WIDTH); visibleCountSpinner.setPrefWidth(120);
pagination.maxPageIndicatorCountProperty().bind(visibleCountSpinner.valueProperty()); pg.maxPageIndicatorCountProperty().bind(visibleCountSpinner.valueProperty());
var bulletToggle = new ToggleSwitch(); var bulletToggle = new ToggleSwitch();
bulletToggle.selectedProperty().addListener( bulletToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(pagination, Pagination.STYLE_CLASS_BULLET) (obs, old, val) -> Styles.toggleStyleClass(pg, Pagination.STYLE_CLASS_BULLET)
); );
var showArrowsToggle = new ToggleSwitch(); var showArrowsToggle = new ToggleSwitch();
showArrowsToggle.selectedProperty().addListener((obs, old, val) -> { showArrowsToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { if (val != null) {
pagination.setStyle(String.format("-fx-arrows-visible: %s;", val)); pg.setStyle(String.format("-fx-arrows-visible: %s;", val));
} }
}); });
showArrowsToggle.setSelected(true); showArrowsToggle.setSelected(true);
@ -74,17 +143,17 @@ public class PaginationPage extends AbstractPage {
var showPageInfoToggle = new ToggleSwitch(); var showPageInfoToggle = new ToggleSwitch();
showPageInfoToggle.selectedProperty().addListener((obs, old, val) -> { showPageInfoToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) { if (val != null) {
pagination.setStyle(String.format("-fx-page-information-visible: %s;", val)); pg.setStyle(String.format("-fx-page-information-visible: %s;", val));
} }
}); });
showPageInfoToggle.setSelected(true); showPageInfoToggle.setSelected(true);
var disableToggle = new ToggleSwitch(); var disableToggle = new ToggleSwitch();
pagination.disableProperty().bind(disableToggle.selectedProperty()); pg.disableProperty().bind(disableToggle.selectedProperty());
var controls = new GridPane(); var controls = new GridPane();
controls.setHgap(BLOCK_HGAP); controls.setHgap(20);
controls.setVgap(BLOCK_VGAP); controls.setVgap(10);
controls.setAlignment(Pos.CENTER); controls.setAlignment(Pos.CENTER);
controls.add(new Label("Page count"), 0, 0); controls.add(new Label("Page count"), 0, 0);
@ -107,8 +176,13 @@ public class PaginationPage extends AbstractPage {
// ~ // ~
var playground = new VBox(BLOCK_VGAP, pagination, new Separator(), controls); var description = BBCodeParser.createFormattedText("""
playground.setMinHeight(100); The playground demonstrates the most important [i]Pagination[/i] features \
and also serves as an object for monkey testing."""
);
var playground = new VBox(VGAP_10, description, pg, new Separator(), controls);
playground.setPrefHeight(200);
return playground; return playground;
} }

@ -1,168 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.InlineDatePicker;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Popover.ArrowLocation;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import java.time.LocalDate;
import java.time.ZoneId;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class PopoverPage extends AbstractPage {
public static final String NAME = "Popover";
@Override
public String getName() {
return NAME;
}
public PopoverPage() {
super();
setUserContent(new VBox(Page.PAGE_VGAP,
new HBox(PAGE_HGAP, textSample(), datePickerSample(), dialogSample()),
positionSample()
));
}
private SampleBlock textSample() {
var popover = new Popover(createTextFlow(30));
popover.setTitle("Lorem Ipsum");
popover.setHeaderAlwaysVisible(true);
popover.setDetachable(true);
var link = createHyperlink("Click me");
link.setOnAction(e -> popover.show(link));
return new SampleBlock("Text", link);
}
private SampleBlock datePickerSample() {
var datePicker = new InlineDatePicker();
datePicker.setValue(LocalDate.now(ZoneId.systemDefault()));
var popover = new Popover(datePicker);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
var link = createHyperlink("Click me");
link.setOnAction(e -> popover.show(link));
new CSSFragment("""
.popover .date-picker-popup {
-color-date-border: transparent;
-color-date-bg: transparent;
-color-date-day-bg: transparent;
-color-date-month-year-bg: transparent;
-color-date-day-bg-hover: -color-bg-subtle;
}
"""
).addTo(link);
return new SampleBlock("Date Picker", link);
}
private SampleBlock dialogSample() {
var root = new VBox(BLOCK_VGAP);
var popover = new Popover(root);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
var icon = new FontIcon(Feather.ALERT_TRIANGLE);
icon.setIconSize(32); // not always works
icon.setStyle(
"-fx-icon-size:32px;-fx-icon-color:-color-warning-fg;-fx-fill:-color-warning-fg;" + icon.getStyle());
var label = new Label(FAKER.chuckNorris().fact(), icon);
label.setStyle("-fx-graphic-text-gap:10;");
label.setWrapText(true);
label.setMaxWidth(300);
label.setMaxHeight(Double.MAX_VALUE);
root.getChildren().add(label);
var yesBtn = new Button("Yes");
yesBtn.setDefaultButton(true);
yesBtn.setOnAction(e -> popover.hide());
var noBtn = new Button("No");
noBtn.setOnAction(e -> popover.hide());
var box = new HBox(10, yesBtn, noBtn);
box.setPadding(new Insets(0, 0, 0, 42));
box.setAlignment(Pos.CENTER_LEFT);
root.getChildren().add(box);
var link = createHyperlink("Click me");
link.setOnAction(e -> popover.show(link));
return new SampleBlock("Dialog", link);
}
private SampleBlock positionSample() {
var grid = new GridPane();
grid.setHgap(BLOCK_HGAP);
grid.setVgap(BLOCK_VGAP);
grid.add(createArrowPositionBlock(ArrowLocation.TOP_LEFT), 0, 0);
grid.add(createArrowPositionBlock(ArrowLocation.TOP_CENTER), 0, 1);
grid.add(createArrowPositionBlock(ArrowLocation.TOP_RIGHT), 0, 2);
grid.add(createArrowPositionBlock(ArrowLocation.RIGHT_TOP), 1, 0);
grid.add(createArrowPositionBlock(ArrowLocation.RIGHT_CENTER), 1, 1);
grid.add(createArrowPositionBlock(ArrowLocation.RIGHT_BOTTOM), 1, 2);
grid.add(createArrowPositionBlock(ArrowLocation.BOTTOM_LEFT), 2, 0);
grid.add(createArrowPositionBlock(ArrowLocation.BOTTOM_CENTER), 2, 1);
grid.add(createArrowPositionBlock(ArrowLocation.BOTTOM_RIGHT), 2, 2);
grid.add(createArrowPositionBlock(ArrowLocation.LEFT_TOP), 3, 0);
grid.add(createArrowPositionBlock(ArrowLocation.LEFT_CENTER), 3, 1);
grid.add(createArrowPositionBlock(ArrowLocation.LEFT_BOTTOM), 3, 2);
return new SampleBlock("Position", grid);
}
private Hyperlink createHyperlink(String text) {
Hyperlink hyperlink = new Hyperlink(text);
hyperlink.setMinWidth(50);
hyperlink.setMinHeight(50);
hyperlink.setAlignment(Pos.CENTER_LEFT);
return hyperlink;
}
private TextFlow createTextFlow(int wordCount) {
var textFlow = new TextFlow(new Text(FAKER.lorem().sentence(wordCount)));
textFlow.setPrefWidth(300);
return textFlow;
}
private Hyperlink createArrowPositionBlock(ArrowLocation arrowLocation) {
var popover = new Popover(createTextFlow(50));
popover.setHeaderAlwaysVisible(false);
popover.setArrowLocation(arrowLocation);
var link = createHyperlink(String.valueOf(arrowLocation));
link.setOnAction(e -> popover.show(link));
return link;
}
}

@ -0,0 +1,372 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.theme.CSSFragment;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
// #javafx-bug Indeterminate (animated) progress bar and also progress indicator
// are very resource expensive. It consumes a single CPU core and a lot of memory.
public class ProgressIndicatorPage extends OutlinePage {
public static final String NAME = "ProgressIndicator";
@Override
public String getName() {
return NAME;
}
public ProgressIndicatorPage() {
super();
addFormattedText("""
JavaFX provides the two types of progress indicators: the [i]ProgressIndicator[/i] and \
the [i]ProgressBar[/i]. In addition to them AtlantaFX also provides the \
[i]RingProgressIndicator[/i]."""
);
addSection("Progress Bar", basicBarExample());
addSection("Progress Indicator", basicIndicatorExample());
addSection("Size", barSizeSample());
addSection("Ring Indicator", ringIndicatorExample());
addSection("Indeterminate", indeterminateExample());
addSection("Dynamic Color Change", colorChangeExample());
}
private ExampleBox basicBarExample() {
//snippet_1:start
var bar1 = new ProgressBar(0);
bar1.setPrefWidth(250);
var bar2 = new ProgressBar(0.5);
bar2.setPrefWidth(250);
var bar3 = new ProgressBar(1);
bar3.setPrefWidth(250);
//snippet_1:end
var box = new VBox(VGAP_20, bar1, bar2, bar3);
box.setMinHeight(100);
var description = BBCodeParser.createFormattedText("""
[i]ProgressBar[/i] is a specialization of the [i]ProgressIndicator[/i] \
which is represented as a horizontal bar."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox basicIndicatorExample() {
//snippet_2:start
var ind1 = new ProgressIndicator(0);
ind1.setMinSize(50, 50);
var ind2 = new ProgressIndicator(0.5);
ind2.setMinSize(50, 50);
var ind3 = new ProgressIndicator(1);
ind3.setMinSize(50, 50);
//snippet_2:end
var box = new HBox(VGAP_20, ind1, ind2, ind3);
var description = BBCodeParser.createFormattedText("""
[i]ProgressIndicator[/i] is a circular control which is used for indicating progress, \
either infinite (aka indeterminate) or finite."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox barSizeSample() {
//snippet_3:start
var smallBar = new ProgressBar(0.5);
smallBar.setPrefWidth(250);
smallBar.getStyleClass().add(Styles.SMALL);
var mediumBar = new ProgressBar(0.5);
mediumBar.setPrefWidth(250);
mediumBar.getStyleClass().add(Styles.MEDIUM);
var largeBar = new ProgressBar(0.5);
largeBar.setPrefWidth(250);
largeBar.getStyleClass().add(Styles.LARGE);
//snippet_3:end
var box = new VBox(
VGAP_20,
new HBox(HGAP_30, smallBar, new Text("small")),
new HBox(HGAP_30, mediumBar, new Text("medium")),
new HBox(HGAP_30, largeBar, new Text("large"))
);
box.setAlignment(Pos.TOP_LEFT);
box.getChildren().forEach(c -> ((HBox) c).setAlignment(Pos.CENTER_LEFT));
var description = BBCodeParser.createFormattedText("""
To change the height of the [i]ProgressBar[/i], you can apply the \
[code]Styles.SMALL[/code] or [code]Styles.LARGE[/code] style classes, respectively. \
Use the large variant if you want to display a text on top of the [i]ProgressBar[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox ringIndicatorExample() {
//snippet_4:start
var basicInd = new RingProgressIndicator(0, false);
var customTextInd = new RingProgressIndicator(0.5, false);
customTextInd.setMinSize(75, 75);
customTextInd.setStringConverter(new StringConverter<>() {
@Override
public String toString(Double progress) {
return (int) Math.ceil(progress * 100) + "°";
}
@Override
public Double fromString(String progress) {
return 0d;
}
});
var reverseInd = new RingProgressIndicator(0.25, true);
reverseInd.setMinSize(150, 150);
var reverseLabel = new Label("25%");
reverseLabel.getStyleClass().add(Styles.TITLE_4);
var reverseBtn = new Button("", new FontIcon(Feather.PLAY));
reverseBtn.getStyleClass().addAll(
Styles.BUTTON_CIRCLE, Styles.FLAT
);
reverseBtn.disableProperty().bind(
reverseInd.progressProperty().greaterThan(0.25)
);
reverseBtn.setOnAction(evt1 -> {
var task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int steps = 100;
for (int i = 25; i <= steps; i++) {
Thread.sleep(100);
updateProgress(i, steps);
updateMessage(i + "%");
}
return null;
}
};
// reset properties, so we can start a new task
task.setOnSucceeded(evt2 -> {
reverseInd.progressProperty().unbind();
reverseLabel.textProperty().unbind();
reverseInd.setProgress(0.25);
reverseLabel.setText("25%");
});
reverseInd.progressProperty().bind(task.progressProperty());
reverseLabel.textProperty().bind(task.messageProperty());
new Thread(task).start();
});
var reverseGraphic = new VBox(10, reverseLabel, reverseBtn);
reverseGraphic.setAlignment(Pos.CENTER);
reverseInd.setGraphic(reverseGraphic);
//snippet_4:end
var box = new HBox(HGAP_20, basicInd, customTextInd, reverseInd);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The [i]RingProgressIndicator[/i] is a type of progress indicator \
that displays progress as a ring that gradually empties out as a task is completed."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
private ExampleBox indeterminateExample() {
//snippet_5:start
var barToggle = new ToggleButton("Start");
barToggle.textProperty().bind(Bindings.createStringBinding(
() -> barToggle.isSelected() ? "Stop" : "Start",
barToggle.selectedProperty()
));
var bar = new ProgressBar(0);
bar.progressProperty().bind(Bindings.createDoubleBinding(
() -> barToggle.isSelected() ? -1d : 0d,
barToggle.selectedProperty()
));
var indicatorToggle = new ToggleButton("Start");
indicatorToggle.textProperty().bind(Bindings.createStringBinding(
() -> indicatorToggle.isSelected() ? "Stop" : "Start",
indicatorToggle.selectedProperty()
));
var indicator = new ProgressIndicator(0);
indicator.setMinSize(60, 60);
indicator.progressProperty().bind(Bindings.createDoubleBinding(
() -> indicatorToggle.isSelected() ? -1d : 0d,
indicatorToggle.selectedProperty()
));
var ringToggle = new ToggleButton("Start");
ringToggle.textProperty().bind(Bindings.createStringBinding(
() -> ringToggle.isSelected() ? "Stop" : "Start",
ringToggle.selectedProperty()
));
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(75, 75);
ring.progressProperty().bind(Bindings.createDoubleBinding(
() -> ringToggle.isSelected() ? -1d : 0d,
ringToggle.selectedProperty()
));
//snippet_5:end
var grid = new GridPane();
grid.setHgap(60);
grid.setVgap(10);
grid.getColumnConstraints().setAll(
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true)
);
grid.getRowConstraints().setAll(
new RowConstraints(-1, -1, -1, Priority.ALWAYS, VPos.CENTER, true),
new RowConstraints(-1, -1, -1, Priority.NEVER, VPos.CENTER, true)
);
grid.addColumn(0, bar, barToggle);
grid.addColumn(1, indicator, indicatorToggle);
grid.addColumn(2, ring, ringToggle);
var description = BBCodeParser.createFormattedText("""
Animated JavaFX progress indicators aren't cheap. They can consume quite a lot of CPU time."""
);
return new ExampleBox(grid, new Snippet(getClass(), 5), description);
}
private ExampleBox colorChangeExample() {
var dataClass = """
.example:success .progress-bar {
-color-progress-bar-fill: -color-success-emphasis;
}
.example:danger .progress-bar {
-color-progress-bar-fill: -color-danger-emphasis;
}
.example:success .label,
.example:danger .label {
-fx-text-fill: -color-fg-emphasis;
}""";
//snippet_6:start
var bar = new ProgressBar(0);
bar.getStyleClass().add(Styles.LARGE);
bar.setPrefWidth(300);
bar.setMaxWidth(300);
var barText = new Label();
var barStack = new StackPane(bar, barText);
barStack.getStyleClass().add("example");
barStack.setPrefWidth(300);
barStack.setMaxWidth(300);
var runBtn = new Button("Start");
runBtn.disableProperty().bind(
bar.progressProperty().greaterThan(0)
);
var content = new VBox(VGAP_10, barStack, runBtn);
content.setAlignment(Pos.CENTER_LEFT);
content.setPrefHeight(200);
// .example:success .progress-bar {
// -color-progress-bar-fill: -color-success-emphasis;
// }
// .example:danger .progress-bar {
// -color-progress-bar-fill: -color-danger-emphasis;
// }
// .example:success .label,
// .example:danger .label {
// -fx-text-fill: -color-fg-emphasis;
// }
new CSSFragment(dataClass).addTo(content);
bar.progressProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
if (val.floatValue() > 0.80) {
barStack.pseudoClassStateChanged(Styles.STATE_DANGER, true);
} else if (val.floatValue() > 0.47) {
barStack.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
}
});
runBtn.setOnAction(evt1 -> {
var task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int steps = 1_000;
for (int i = 0; i < steps; i++) {
Thread.sleep(10);
updateProgress(i, steps);
updateMessage(String.valueOf(i));
}
return null;
}
};
// reset properties, so we can start a new task
task.setOnSucceeded(evt2 -> {
bar.progressProperty().unbind();
barText.textProperty().unbind();
bar.setProgress(0);
barText.setText(null);
barStack.pseudoClassStateChanged(Styles.STATE_SUCCESS, false);
barStack.pseudoClassStateChanged(Styles.STATE_DANGER, false);
});
bar.progressProperty().bind(task.progressProperty());
barText.textProperty().bind(task.messageProperty());
new Thread(task).start();
});
//snippet_6:end
var description = BBCodeParser.createFormattedText("""
This example demonstrates how looked-up color variables can be used to \
change the progress bar color while the task is in progress."""
);
return new ExampleBox(content, new Snippet(getClass(), 6), description);
}
}

@ -1,336 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BUTTON_CIRCLE;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.base.theme.Styles.LARGE;
import static atlantafx.base.theme.Styles.MEDIUM;
import static atlantafx.base.theme.Styles.SMALL;
import static atlantafx.base.theme.Styles.TITLE_4;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Task;
import javafx.css.PseudoClass;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
// #javafx-bug Indeterminate (animated) progress bar and also progress indicator
// are very resource expensive. It consumes a single CPU core and a lot of memory.
public class ProgressPage extends AbstractPage {
public static final String NAME = "Progress";
@Override
public String getName() {
return NAME;
}
public ProgressPage() {
super();
var grid = new GridPane();
grid.setHgap(Page.PAGE_HGAP);
grid.setVgap(Page.PAGE_VGAP);
grid.add(basicBarSample(), 0, 0);
grid.add(basicIndicatorSample(), 1, 0);
grid.add(ringIndicatorSample(), 0, 1);
grid.add(barSizeSample(), 1, 1);
grid.add(indeterminateSample(), 0, 2);
grid.add(colorChangeSample(), 1, 2);
setUserContent(grid);
}
private SampleBlock basicBarSample() {
var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
createBar(0, false),
createBar(0.5, false),
createBar(1, false),
createBar(0.5, true)
);
flowPane.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Progress Bar", flowPane);
}
private SampleBlock basicIndicatorSample() {
var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
createIndicator(0, false),
createIndicator(0.5, false),
createIndicator(1, false),
createIndicator(0.5, true)
);
flowPane.setAlignment(Pos.TOP_LEFT);
return new SampleBlock("Progress Indicator", flowPane);
}
private SampleBlock barSizeSample() {
var container = new VBox(
BLOCK_VGAP,
new HBox(20, createBar(0.5, false, SMALL), new Text("small")),
new HBox(20, createBar(0.5, false, MEDIUM), new Text("medium")),
new HBox(20, createBar(0.5, false, LARGE), new Text("large"))
);
container.setAlignment(Pos.TOP_LEFT);
container.getChildren().forEach(c -> ((HBox) c).setAlignment(Pos.CENTER_LEFT));
return new SampleBlock("Size", container);
}
private SampleBlock ringIndicatorSample() {
var basicIndicator = new RingProgressIndicator(0, false);
var customTextIndicator = new RingProgressIndicator(0.5, false);
customTextIndicator.setMinSize(75, 75);
customTextIndicator.setStringConverter(new StringConverter<>() {
@Override
public String toString(Double progress) {
return (int) Math.ceil(progress * 100) + "°";
}
@Override
public Double fromString(String progress) {
return 0d;
}
});
var reverseIndicator = new RingProgressIndicator(0.25, true);
reverseIndicator.setMinSize(150, 150);
var reverseIndicatorLabel = new Label("25%");
reverseIndicatorLabel.getStyleClass().add(TITLE_4);
var reversePlayButton = new Button("", new FontIcon(Feather.PLAY));
reversePlayButton.getStyleClass().addAll(BUTTON_CIRCLE, FLAT);
reversePlayButton.disableProperty().bind(reverseIndicator.progressProperty().greaterThan(0.25));
reversePlayButton.setOnAction(e1 -> {
var task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int steps = 100;
for (int i = 25; i <= steps; i++) {
Thread.sleep(100);
updateProgress(i, steps);
updateMessage(i + "%");
}
return null;
}
};
// reset properties, so we can start a new task
task.setOnSucceeded(e2 -> {
reverseIndicator.progressProperty().unbind();
reverseIndicatorLabel.textProperty().unbind();
reverseIndicator.setProgress(0.25);
reverseIndicatorLabel.setText("25%");
});
reverseIndicator.progressProperty().bind(task.progressProperty());
reverseIndicatorLabel.textProperty().bind(task.messageProperty());
new Thread(task).start();
});
var reverseBox = new VBox(10, reverseIndicatorLabel, reversePlayButton);
reverseBox.setAlignment(Pos.CENTER);
reverseIndicator.setGraphic(reverseBox);
// ~
var box = new HBox(BLOCK_HGAP, basicIndicator, customTextIndicator, reverseIndicator);
box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Ring Indicator", box);
}
private SampleBlock indeterminateSample() {
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(BLOCK_VGAP);
grid.getColumnConstraints().setAll(
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true),
new ColumnConstraints(-1, -1, -1, Priority.NEVER, HPos.CENTER, true)
);
grid.getRowConstraints().setAll(
new RowConstraints(-1, -1, -1, Priority.ALWAYS, VPos.CENTER, true),
new RowConstraints(-1, -1, -1, Priority.NEVER, VPos.CENTER, true)
);
var barToggle = new ToggleButton("Start");
barToggle.textProperty().bind(Bindings.createStringBinding(
() -> barToggle.isSelected() ? "Stop" : "Start", barToggle.selectedProperty())
);
var bar = createBar(0, false);
bar.progressProperty().bind(Bindings.createDoubleBinding(
() -> barToggle.isSelected() ? -1d : 0d, barToggle.selectedProperty())
);
grid.add(bar, 0, 0);
grid.add(barToggle, 0, 1);
var indicatorToggle = new ToggleButton("Start");
indicatorToggle.textProperty().bind(Bindings.createStringBinding(
() -> indicatorToggle.isSelected() ? "Stop" : "Start", indicatorToggle.selectedProperty())
);
var indicator = createIndicator(0, false);
indicator.setPrefSize(75, 75);
indicator.progressProperty().bind(Bindings.createDoubleBinding(
() -> indicatorToggle.isSelected() ? -1d : 0d, indicatorToggle.selectedProperty())
);
grid.add(indicator, 1, 0);
grid.add(indicatorToggle, 1, 1);
var ringToggle = new ToggleButton("Start");
ringToggle.textProperty().bind(Bindings.createStringBinding(
() -> ringToggle.isSelected() ? "Stop" : "Start", ringToggle.selectedProperty())
);
var ring = new RingProgressIndicator(0, false);
ring.setMinSize(75, 75);
ring.progressProperty().bind(Bindings.createDoubleBinding(
() -> ringToggle.isSelected() ? -1d : 0d, ringToggle.selectedProperty())
);
grid.add(ring, 2, 0);
grid.add(ringToggle, 2, 1);
return new SampleBlock("Indeterminate", grid,
"Animated JavaFX progress indicators aren't cheap. They can consume quite a lot of CPU time."
);
}
private SampleBlock colorChangeSample() {
final var stateSuccess = PseudoClass.getPseudoClass("state-success");
final var stateDanger = PseudoClass.getPseudoClass("state-danger");
final var width = 300;
var bar = new ProgressBar(0);
bar.getStyleClass().add(LARGE);
bar.setPrefWidth(width);
bar.setMaxWidth(width);
var barText = new Label();
var barStack = new StackPane(bar, barText);
barStack.getStyleClass().add("example");
barStack.setPrefWidth(width);
barStack.setMaxWidth(width);
var runBtn = new Button("Start");
runBtn.disableProperty().bind(bar.progressProperty().greaterThan(0));
// ~
var content = new VBox(BLOCK_VGAP);
content.getChildren().setAll(barStack, runBtn);
content.setAlignment(Pos.CENTER_LEFT);
content.setPrefHeight(200);
bar.progressProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
if (val.floatValue() > 0.80) {
barStack.pseudoClassStateChanged(stateDanger, true);
} else if (val.floatValue() > 0.47) {
barStack.pseudoClassStateChanged(stateSuccess, true);
}
});
new CSSFragment("""
.example:state-success .progress-bar {
-color-progress-bar-fill: -color-success-emphasis;
}
.example:state-danger .progress-bar {
-color-progress-bar-fill: -color-danger-emphasis;
}
.example:state-success .label,
.example:state-danger .label {
-fx-text-fill: -color-fg-emphasis;
}
""").addTo(content);
runBtn.setOnAction(e1 -> {
var task = new Task<Void>() {
@Override
protected Void call() throws Exception {
int steps = 1_000;
for (int i = 0; i < steps; i++) {
Thread.sleep(10);
updateProgress(i, steps);
updateMessage(String.valueOf(i));
}
return null;
}
};
// reset properties, so we can start a new task
task.setOnSucceeded(e2 -> {
bar.progressProperty().unbind();
barText.textProperty().unbind();
bar.setProgress(0);
barText.setText(null);
barStack.pseudoClassStateChanged(stateSuccess, false);
barStack.pseudoClassStateChanged(stateDanger, false);
});
bar.progressProperty().bind(task.progressProperty());
barText.textProperty().bind(task.messageProperty());
new Thread(task).start();
});
return new SampleBlock("Dynamic Color Change", content);
}
private ProgressBar createBar(double progress, boolean disabled, String... styleClasses) {
var bar = new ProgressBar(progress);
bar.getStyleClass().addAll(styleClasses);
bar.setDisable(disabled);
return bar;
}
private ProgressIndicator createIndicator(double progress, boolean disabled) {
var indicator = new ProgressIndicator(progress);
indicator.setMinSize(50, 50);
indicator.setMaxSize(50, 50);
indicator.setDisable(disabled);
return indicator;
}
}

@ -2,17 +2,15 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
public class RadioButtonPage extends AbstractPage { public class RadioButtonPage extends OutlinePage {
public static final String NAME = "RadioButton"; public static final String NAME = "RadioButton";
@ -23,30 +21,30 @@ public class RadioButtonPage extends AbstractPage {
public RadioButtonPage() { public RadioButtonPage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP, addFormattedText("""
basicSample(), [i]RadioButton[/i]'s create a series of items where only one item can be selected.""");
groupSample(), addSection("Usage", usageExample());
disabledSample() addSection("Toggle Group", toggleGroupExample());
));
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
//snippet_1:start
var radio1 = new RadioButton("_Check Me"); var radio1 = new RadioButton("_Check Me");
radio1.setMnemonicParsing(true); radio1.setMnemonicParsing(true);
var radio2 = new RadioButton("Check Me"); var radio2 = new RadioButton("Check Me");
//snippet_1:end
return new SampleBlock("Basic", new VBox(BLOCK_VGAP, radio1, radio2)); var box = new VBox(VGAP_10, radio1, radio2);
var description = BBCodeParser.createFormattedText("""
A radio button control can be either selected or deselected.""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock disabledSample() { private ExampleBox toggleGroupExample() {
var radio = new RadioButton("Check Me"); //snippet_2:start
radio.setDisable(true);
return new SampleBlock("Disabled", radio);
}
private SampleBlock groupSample() {
var group = new ToggleGroup(); var group = new ToggleGroup();
var musicRadio = new RadioButton("Music"); var musicRadio = new RadioButton("Music");
@ -58,7 +56,14 @@ public class RadioButtonPage extends AbstractPage {
var videosRadio = new RadioButton("Videos"); var videosRadio = new RadioButton("Videos");
videosRadio.setToggleGroup(group); videosRadio.setToggleGroup(group);
//snippet_2:end
return new SampleBlock("Toggle Group", new VBox(5, musicRadio, imagesRadio, videosRadio)); var box = new VBox(VGAP_10, musicRadio, imagesRadio, videosRadio);
var description = BBCodeParser.createFormattedText("""
Typically radio buttons are combined into a group where only one button \
at a time can be selected. This behavior distinguishes them from toggle buttons, \
because all toggle buttons in a group can be in a deselected state.""");
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
} }

@ -3,8 +3,6 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@ -15,7 +13,6 @@ import javafx.scene.layout.VBox;
public class ScrollPanePage extends AbstractPage { public class ScrollPanePage extends AbstractPage {
public static final String NAME = "ScrollPane"; public static final String NAME = "ScrollPane";
private static final int SPACING = 1;
@Override @Override
public String getName() { public String getName() {
@ -24,63 +21,69 @@ public class ScrollPanePage extends AbstractPage {
public ScrollPanePage() { public ScrollPanePage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_VGAP, Page.PAGE_HGAP, addFormattedText("""
horizontalScrollSample(), A control that provides a scrolled, clipped viewport of its contents. \
verticalScrollSample(), It allows the user to scroll the content around either directly (panning) \
gridScrollSample(), or by using scroll bars.
disabledSample()
The [i]ScrollPane[/i] allows specification of the scroll bar policy, which \
determines when scroll bars are displayed: always, never, or only when they \
are needed. The scroll bar policy can be specified independently for the \
horizontal and vertical scroll bars."""
);
addNode(new FlowPane(
40, 40,
horizontalScrollExample(),
verticalScrollExample(),
gridScrollExample()
)); ));
} }
private SampleBlock horizontalScrollSample() { private ScrollPane horizontalScrollExample() {
var scrollPane = new ScrollPane(); var scrollPane = new ScrollPane();
scrollPane.setMaxHeight(100); scrollPane.setMaxHeight(100);
scrollPane.setMaxWidth(300); scrollPane.setMaxWidth(300);
scrollPane.setContent(new HBox(SPACING, scrollPane.setContent(new HBox(1,
createRegion(200, 100, "-color-success-emphasis"), createRegion(200, 100, "-color-success-emphasis"),
createRegion(200, 100, "-color-danger-emphasis") createRegion(200, 100, "-color-danger-emphasis")
)); ));
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
return new SampleBlock("Horizontal Scrolling", scrollPane); return scrollPane;
} }
private SampleBlock verticalScrollSample() { private ScrollPane verticalScrollExample() {
var scrollPane = new ScrollPane(); var scrollPane = new ScrollPane();
scrollPane.setMaxHeight(100); scrollPane.setMaxHeight(100);
scrollPane.setMaxWidth(300); scrollPane.setMaxWidth(300);
scrollPane.setContent(new VBox(SPACING, scrollPane.setContent(new VBox(1,
createRegion(300, 75, "-color-success-emphasis"), createRegion(300, 75, "-color-success-emphasis"),
createRegion(300, 75, "-color-danger-emphasis") createRegion(300, 75, "-color-danger-emphasis")
)); ));
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
return new SampleBlock("Vertical Scrolling", scrollPane); return scrollPane;
} }
private SampleBlock gridScrollSample() { private ScrollPane gridScrollExample() {
var grid = new GridPane(); var grid = new GridPane();
grid.add(createRegion(200, 75, "-color-success-emphasis"), 0, 0); grid.setHgap(1);
grid.add(createRegion(200, 75, "-color-danger-emphasis"), 1, 0); grid.setVgap(1);
grid.add(createRegion(200, 75, "-color-danger-emphasis"), 0, 1); grid.addColumn(0,
grid.add(createRegion(200, 75, "-color-success-emphasis"), 1, 1); createRegion(200, 75, "-color-success-emphasis"),
grid.setHgap(SPACING); createRegion(200, 75, "-color-danger-emphasis")
grid.setVgap(SPACING); );
grid.addColumn(1,
createRegion(200, 75, "-color-danger-emphasis"),
createRegion(200, 75, "-color-success-emphasis")
);
var gridScroll = new ScrollPane(); var gridScroll = new ScrollPane(grid);
gridScroll.setMaxHeight(100); gridScroll.setMaxHeight(100);
gridScroll.setMaxWidth(300); gridScroll.setMaxWidth(300);
gridScroll.setContent(grid);
return new SampleBlock("Scrolling", gridScroll); return gridScroll;
}
private SampleBlock disabledSample() {
var block = gridScrollSample();
block.setTitle("Disabled");
block.getContent().setDisable(true);
return block;
} }
private Region createRegion(int width, int height, String bg) { private Region createRegion(int width, int height, String bg) {

@ -2,28 +2,22 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
import static javafx.geometry.Pos.CENTER;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Separator; import javafx.scene.control.Separator;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
public final class SeparatorPage extends AbstractPage { public final class SeparatorPage extends OutlinePage {
public static final String NAME = "Separator"; public static final String NAME = "Separator";
private static final int SPACING = 50;
private static final int PANE_SIZE = 100;
@Override @Override
public String getName() { public String getName() {
@ -32,79 +26,85 @@ public final class SeparatorPage extends AbstractPage {
public SeparatorPage() { public SeparatorPage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP, addFormattedText("""
orientationSample(), A horizontal or vertical separator line. A horizontal separator occupies \
sizeSample() the full horizontal space allocated to it (less padding), and a vertical \
)); separator occupies the full vertical space allocated to it (less padding)."""
);
addSection("Orientation", orientationExample());
addSection("Size", sizeExample());
} }
private SampleBlock orientationSample() { private ExampleBox orientationExample() {
var hBox = new HBox( //snippet_1:start
createPane("Left", VERTICAL), var hbox = new HBox(
new Separator(VERTICAL), createPane("Left", Orientation.VERTICAL),
createPane("Right", VERTICAL) new Separator(Orientation.VERTICAL),
createPane("Right", Orientation.VERTICAL)
); );
hBox.setAlignment(CENTER);
var vBox = new VBox( var vbox = new VBox(
createPane("Top", HORIZONTAL), createPane("Top", Orientation.HORIZONTAL),
new Separator(HORIZONTAL), new Separator(Orientation.HORIZONTAL),
createPane("Bottom", HORIZONTAL) createPane("Bottom", Orientation.HORIZONTAL)
); );
vBox.setAlignment(CENTER); //snippet_1:end
return new SampleBlock("Orientation", new HBox(SPACING, hBox, vBox)); var box = new HBox(50, hbox, vbox);
return new ExampleBox(box, new Snippet(getClass(), 1));
} }
private SampleBlock sizeSample() { private ExampleBox sizeExample() {
var smallSep = new Separator(VERTICAL); //snippet_2:start
var smallSep = new Separator(Orientation.VERTICAL);
smallSep.getStyleClass().add(Styles.SMALL); smallSep.getStyleClass().add(Styles.SMALL);
var smallBox = new HBox( var smallBox = new HBox(
createPane("Left", VERTICAL), createPane("Left", Orientation.VERTICAL),
smallSep, smallSep,
createPane("Right", VERTICAL) createPane("Right", Orientation.VERTICAL)
); );
smallBox.setAlignment(CENTER);
var mediumSep = new Separator(VERTICAL); var mediumSep = new Separator(Orientation.VERTICAL);
mediumSep.getStyleClass().add(Styles.MEDIUM); mediumSep.getStyleClass().add(Styles.MEDIUM);
var mediumBox = new HBox( var mediumBox = new HBox(
createPane("Left", VERTICAL), createPane("Left", Orientation.VERTICAL),
mediumSep, mediumSep,
createPane("Right", VERTICAL) createPane("Right", Orientation.VERTICAL)
); );
mediumBox.setAlignment(CENTER);
var largeSep = new Separator(VERTICAL); var largeSep = new Separator(Orientation.VERTICAL);
largeSep.getStyleClass().add(Styles.LARGE); largeSep.getStyleClass().add(Styles.LARGE);
var largeBox = new HBox( var largeBox = new HBox(
createPane("Left", VERTICAL), createPane("Left", Orientation.VERTICAL),
largeSep, largeSep,
createPane("Right", VERTICAL) createPane("Right", Orientation.VERTICAL)
); );
largeBox.setAlignment(CENTER); //snippet_2:end
return new SampleBlock("Size", new HBox(SPACING, smallBox, mediumBox, largeBox)); var grid = new GridPane();
grid.setHgap(50);
grid.setVgap(50);
grid.addRow(0, smallBox, mediumBox);
grid.addRow(1, largeBox);
return new ExampleBox(grid, new Snippet(getClass(), 2));
} }
private Pane createPane(String text, Orientation orientation) { private Pane createPane(String text, Orientation orientation) {
var pane = new StackPane(); var pane = new StackPane(new Label(text));
pane.getChildren().setAll(new Label(text));
pane.getStyleClass().add("bordered"); pane.getStyleClass().add("bordered");
pane.setMinSize(100, 100);
if (orientation == HORIZONTAL) { if (orientation == Orientation.HORIZONTAL) {
pane.setMinHeight(PANE_SIZE); pane.setPrefHeight(100);
pane.setPrefHeight(PANE_SIZE); pane.setMaxHeight(100);
pane.setMaxHeight(PANE_SIZE);
pane.setMinWidth(PANE_SIZE);
} }
if (orientation == VERTICAL) { if (orientation == Orientation.VERTICAL) {
pane.setMinWidth(PANE_SIZE); pane.setPrefWidth(100);
pane.setPrefWidth(PANE_SIZE); pane.setMaxWidth(100);
pane.setMaxWidth(PANE_SIZE);
pane.setMinHeight(PANE_SIZE);
} }
return pane; return pane;

@ -2,23 +2,18 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static javafx.geometry.Orientation.VERTICAL;
import atlantafx.base.controls.ProgressSliderSkin; import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import javafx.geometry.Orientation;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
public class SliderPage extends AbstractPage { public class SliderPage extends OutlinePage {
public static final String NAME = "Slider"; public static final String NAME = "Slider";
private static final int SLIDER_SIZE = 180;
private static final int SPACING = 20;
@Override @Override
public String getName() { public String getName() {
@ -27,32 +22,44 @@ public class SliderPage extends AbstractPage {
public SliderPage() { public SliderPage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP, addFormattedText("""
basicSample(), The [i]Slider[/i] control is used to display a continuous or discrete range of valid numeric \
smallSample(), choices. It is typically represented visually as having a [i]track[/i] and a [i]knob[/i] \
largeSample(), or [i]thumb[/i] which is dragged within the track. The [i]Slider[/i] can optionally show tick \
disabledSample() marks and labels indicating the different slider position values.
));
AtlantaFX also provides the [b]ProgressSliderSkin[/b], which implements color support for \
[i]Slider[/i] progress indication. Additionally, it adds the [code]Styles.SMALL[/code] and \
[code]Styles.LARGE[/code] style class modifiers to change the [i]Slider[/i] size."""
);
addSection("Usage", usageExample());
addSection("Small", smallExample());
addSection("Large", largeExample());
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
//snippet_1:start
var hSlider = new Slider(1, 5, 3); var hSlider = new Slider(1, 5, 3);
var hTickSlider = createTickSlider(); var hTickSlider = createTickSlider();
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider)); hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
var vSlider = new Slider(1, 5, 3); var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL); vSlider.setOrientation(Orientation.VERTICAL);
var vTickSlider = createTickSlider(); var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL); vTickSlider.setOrientation(Orientation.VERTICAL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider)); vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
//snippet_1:end
return new SampleBlock("Basic", createContent(hSlider, hTickSlider, vSlider, vTickSlider)); var box = createGrid(hSlider, hTickSlider, vSlider, vTickSlider);
return new ExampleBox(box, new Snippet(getClass(), 1));
} }
private Pane smallSample() { private ExampleBox smallExample() {
//snippet_2:start
var hSlider = new Slider(1, 5, 3); var hSlider = new Slider(1, 5, 3);
hSlider.getStyleClass().add(Styles.SMALL); hSlider.getStyleClass().add(Styles.SMALL);
@ -61,18 +68,22 @@ public class SliderPage extends AbstractPage {
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider)); hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
var vSlider = new Slider(1, 5, 3); var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL); vSlider.setOrientation(Orientation.VERTICAL);
vSlider.getStyleClass().add(Styles.SMALL); vSlider.getStyleClass().add(Styles.SMALL);
var vTickSlider = createTickSlider(); var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL); vTickSlider.setOrientation(Orientation.VERTICAL);
vTickSlider.getStyleClass().add(Styles.SMALL); vTickSlider.getStyleClass().add(Styles.SMALL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider)); vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
//snippet_2:end
return new SampleBlock("Small", createContent(hSlider, hTickSlider, vSlider, vTickSlider)); var box = createGrid(hSlider, hTickSlider, vSlider, vTickSlider);
return new ExampleBox(box, new Snippet(getClass(), 2));
} }
private Pane largeSample() { private ExampleBox largeExample() {
//snippet_3:start
var hSlider = new Slider(1, 5, 3); var hSlider = new Slider(1, 5, 3);
hSlider.getStyleClass().add(Styles.LARGE); hSlider.getStyleClass().add(Styles.LARGE);
@ -81,35 +92,18 @@ public class SliderPage extends AbstractPage {
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider)); hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
var vSlider = new Slider(1, 5, 3); var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL); vSlider.setOrientation(Orientation.VERTICAL);
vSlider.getStyleClass().add(Styles.LARGE); vSlider.getStyleClass().add(Styles.LARGE);
var vTickSlider = createTickSlider(); var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL); vTickSlider.setOrientation(Orientation.VERTICAL);
vTickSlider.getStyleClass().add(Styles.LARGE); vTickSlider.getStyleClass().add(Styles.LARGE);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider)); vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
//snippet_3:end
return new SampleBlock("Large", createContent(hSlider, hTickSlider, vSlider, vTickSlider)); var box = createGrid(hSlider, hTickSlider, vSlider, vTickSlider);
}
private Pane disabledSample() { return new ExampleBox(box, new Snippet(getClass(), 3));
var hSlider = new Slider(1, 5, 3);
hSlider.setDisable(true);
var hTickSlider = createTickSlider();
hTickSlider.setSkin(new ProgressSliderSkin(hTickSlider));
hTickSlider.setDisable(true);
var vSlider = new Slider(1, 5, 3);
vSlider.setOrientation(VERTICAL);
vSlider.setDisable(true);
var vTickSlider = createTickSlider();
vTickSlider.setOrientation(VERTICAL);
vTickSlider.setSkin(new ProgressSliderSkin(vTickSlider));
vTickSlider.setDisable(true);
return new SampleBlock("Disabled", createContent(hSlider, hTickSlider, vSlider, vTickSlider));
} }
private Slider createTickSlider() { private Slider createTickSlider() {
@ -123,22 +117,20 @@ public class SliderPage extends AbstractPage {
return slider; return slider;
} }
private GridPane createContent(Slider h1, Slider h2, Slider v1, Slider v2) { private GridPane createGrid(Slider hs1, Slider hs2, Slider vs1, Slider vs2) {
var grid = new GridPane(); var grid = new GridPane();
grid.setVgap(SPACING); grid.setVgap(20);
grid.setHgap(SPACING); grid.setHgap(20);
h1.setPrefWidth(SLIDER_SIZE); hs1.setMinWidth(200);
h2.setPrefWidth(SLIDER_SIZE); hs2.setMinWidth(200);
vs1.setMinHeight(200);
vs2.setMinHeight(200);
v1.setPrefHeight(SLIDER_SIZE); grid.add(hs1, 0, 0);
v2.setPrefHeight(SLIDER_SIZE); grid.add(hs2, 0, 1);
grid.add(vs1, 1, 0, 1, GridPane.REMAINING);
grid.add(h1, 0, 0); grid.add(vs2, 2, 0, 1, GridPane.REMAINING);
grid.add(h2, 0, 1);
grid.add(v1, 1, 0, 1, GridPane.REMAINING);
grid.add(v2, 2, 0, 1, GridPane.REMAINING);
return grid; return grid;
} }

@ -2,17 +2,19 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.base.util.BBCodeParser;
import atlantafx.base.util.IntegerStringConverter; import atlantafx.base.util.IntegerStringConverter;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import javafx.geometry.Pos;
import javafx.scene.control.Spinner; import javafx.scene.control.Spinner;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
public final class SpinnerPage extends AbstractPage { public final class SpinnerPage extends OutlinePage {
public static final String NAME = "Spinner"; public static final String NAME = "Spinner";
private static final int PREF_WIDTH = 120;
@Override @Override
public String getName() { public String getName() {
@ -21,75 +23,97 @@ public final class SpinnerPage extends AbstractPage {
public SpinnerPage() { public SpinnerPage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP, addPlainText("""
basicSample(), A single line text field that lets the user select a number or an object \
arrowsLeftVerticalSample(), value from an ordered sequence. The user may also be allowed to type a (legal) \
arrowsLeftHorizontalSample(), value directly into the spinner."""
arrowsRightHorizontalSample(), );
arrowsSplitHorizontalSample(), addSection("Usage", usageExample());
arrowsSplitVerticalSample(), addSection("Horizontal", horizontalExample());
disabledSample() addSection("Vertical", verticalExample());
));
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
//snippet_1:start
var spinner = new Spinner<Integer>(1, 10, 1); var spinner = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner); IntegerStringConverter.createFor(spinner);
spinner.setPrefWidth(PREF_WIDTH);
spinner.setEditable(true); spinner.setEditable(true);
return new SampleBlock("Basic", spinner); spinner.setPrefWidth(120);
//snippet_1:end
var box = new HBox(spinner);
var description = BBCodeParser.createFormattedText("""
A [i]Spinner[/i] has a [i]TextField[/i] child component that is responsible \
for displaying and potentially changing the current value of the [i]Spinner[/i], \
which is called the editor. By default the Spinner is non-editable, but input \
can be accepted if the editable property is set to true. The [i]Spinner[/i] editor stays \
in sync with the value factory by listening for changes to the value property \
of the value factory."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock disabledSample() { private ExampleBox horizontalExample() {
var spinner = new Spinner<Integer>(1, 10, 1); //snippet_2:start
spinner.setPrefWidth(PREF_WIDTH); var sp1 = new Spinner<Integer>(1, 10, 1);
spinner.setDisable(true); IntegerStringConverter.createFor(sp1);
return new SampleBlock("Disabled", spinner); sp1.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL);
sp1.setPrefWidth(120);
sp1.setEditable(true);
var sp2 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(sp2);
sp2.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);
sp2.setPrefWidth(120);
sp2.setEditable(true);
var sp3 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(sp3);
sp3.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);
sp3.setPrefWidth(120);
sp3.setEditable(true);
var sp4 = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(sp4);
sp4.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
sp4.setPrefWidth(120);
sp4.setEditable(true);
//snippet_2:end
var grid = new GridPane();
grid.setVgap(VGAP_20);
grid.setHgap(HGAP_30);
grid.addRow(0, captionLabel("STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL"), sp1);
grid.addRow(1, captionLabel("STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL"), sp2);
grid.addRow(2, captionLabel("STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL"), sp3);
grid.addRow(3, captionLabel("STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL"), sp4);
var description = BBCodeParser.createFormattedText("""
The [i]Spinner[/i] also supports several style class modifiers that determine \
the arrows position."""
);
return new ExampleBox(grid, new Snippet(getClass(), 2), description);
} }
private SampleBlock arrowsLeftVerticalSample() { private ExampleBox verticalExample() {
var spinner = new Spinner<Integer>(1, 10, 1); //snippet_3:start
IntegerStringConverter.createFor(spinner); var sp = new Spinner<Integer>(1, 10, 1);
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_VERTICAL); IntegerStringConverter.createFor(sp);
spinner.setPrefWidth(PREF_WIDTH); sp.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);
spinner.setEditable(true); sp.setEditable(true);
return new SampleBlock("Left & Vertical", spinner); sp.setPrefWidth(40);
} //snippet_3:end
private SampleBlock arrowsLeftHorizontalSample() { var box = new HBox(HGAP_30, captionLabel("STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL"), sp);
var spinner = new Spinner<Integer>(1, 10, 1); box.setAlignment(Pos.CENTER_LEFT);
IntegerStringConverter.createFor(spinner);
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_LEFT_HORIZONTAL);
spinner.setPrefWidth(PREF_WIDTH);
spinner.setEditable(true);
return new SampleBlock("Left & Horizontal", spinner);
}
private SampleBlock arrowsRightHorizontalSample() { var description = BBCodeParser.createFormattedText("""
var spinner = new Spinner<Integer>(1, 10, 1); Similar to the previous example, but for the vertical direction."""
IntegerStringConverter.createFor(spinner); );
spinner.getStyleClass().add(Spinner.STYLE_CLASS_ARROWS_ON_RIGHT_HORIZONTAL);
spinner.setPrefWidth(PREF_WIDTH);
spinner.setEditable(true);
return new SampleBlock("Right & Horizontal", spinner);
}
private SampleBlock arrowsSplitHorizontalSample() { return new ExampleBox(box, new Snippet(getClass(), 3), description);
var spinner = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner);
spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
spinner.setPrefWidth(PREF_WIDTH);
spinner.setEditable(true);
return new SampleBlock("Split & Horizontal", spinner);
}
private SampleBlock arrowsSplitVerticalSample() {
var spinner = new Spinner<Integer>(1, 10, 1);
IntegerStringConverter.createFor(spinner);
spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_VERTICAL);
spinner.setEditable(true);
spinner.setPrefWidth(40);
return new SampleBlock("Split & Vertical", spinner);
} }
} }

@ -2,19 +2,18 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
public class SplitPanePage extends AbstractPage { public class SplitPanePage extends OutlinePage {
public static final String NAME = "SplitPane"; public static final String NAME = "SplitPane";
@ -25,75 +24,116 @@ public class SplitPanePage extends AbstractPage {
public SplitPanePage() { public SplitPanePage() {
super(); super();
setUserContent(new FlowPane(
Page.PAGE_VGAP, Page.PAGE_HGAP, addFormattedText("""
hSplitSample(), A control that has two or more sides, each separated by a divider, which can be \
vSplitSample(), dragged by the user to give more space to one of the sides, resulting in the other \
disabledSample(), side shrinking by an equal amount."""
gridSample() );
)); addSection("Horizontal", hSplitExample());
addSection("Vertical", vSplitExample());
addSection("Nested", nestedSplitExample());
addSection("Multiple Dividers", multiDividersSplitExample());
} }
private SampleBlock hSplitSample() { private ExampleBox hSplitExample() {
var splitPane = new SplitPane(); //snippet_1:start
splitPane.setOrientation(Orientation.HORIZONTAL); var sp = new SplitPane(
splitPane.setDividerPositions(0.5); createBox("Left"),
splitPane.getItems().setAll(createBox("Left"), createBox("Right")); createBox("Right")
splitPane.setMinSize(200, 100); );
splitPane.setMaxSize(200, 100); sp.setOrientation(Orientation.HORIZONTAL);
return new SampleBlock("Horizontal", splitPane); sp.setDividerPositions(0.5);
//snippet_1:end
sp.setMinSize(400, 100);
sp.setMaxSize(400, 100);
var box = new HBox(sp);
return new ExampleBox(box, new Snippet(getClass(), 1));
} }
private SampleBlock vSplitSample() { private ExampleBox vSplitExample() {
var splitPane = new SplitPane(); //snippet_2:start
splitPane.setOrientation(Orientation.VERTICAL); var sp = new SplitPane(
splitPane.setDividerPositions(0.5); createBox("Top"),
splitPane.getItems().setAll(createBox("Top"), createBox("Bottom")); createBox("Bottom")
splitPane.setMinSize(100, 200); );
splitPane.setMaxSize(100, 200); sp.setOrientation(Orientation.VERTICAL);
return new SampleBlock("Vertical", splitPane); sp.setDividerPositions(0.5);
//snippet_2:end
sp.setMinSize(400, 200);
sp.setMaxSize(400, 200);
var box = new HBox(sp);
return new ExampleBox(box, new Snippet(getClass(), 2));
} }
private SampleBlock gridSample() { private ExampleBox nestedSplitExample() {
var topSplitPane = new SplitPane(); //snippet_3:start
var topSplitPane = new SplitPane(
createBox("Quarter 4"),
createBox("Quarter 1")
);
topSplitPane.setOrientation(Orientation.HORIZONTAL); topSplitPane.setOrientation(Orientation.HORIZONTAL);
topSplitPane.setDividerPositions(0.5); topSplitPane.setDividerPositions(0.5);
topSplitPane.getItems().setAll(createBox("Quarter 4"), createBox("Quarter 1"));
VBox.setVgrow(topSplitPane, Priority.ALWAYS); VBox.setVgrow(topSplitPane, Priority.ALWAYS);
var topBox = new VBox(topSplitPane); var topBox = new VBox(topSplitPane);
topBox.setAlignment(Pos.CENTER); topBox.setAlignment(Pos.CENTER);
var bottomSplitPane = new SplitPane(); var bottomSplitPane = new SplitPane(
createBox("Quarter 3"),
createBox("Quarter 2")
);
bottomSplitPane.setOrientation(Orientation.HORIZONTAL); bottomSplitPane.setOrientation(Orientation.HORIZONTAL);
bottomSplitPane.setDividerPositions(0.5); bottomSplitPane.setDividerPositions(0.5);
bottomSplitPane.getItems().setAll(createBox("Quarter 3"), createBox("Quarter 2"));
VBox.setVgrow(bottomSplitPane, Priority.ALWAYS); VBox.setVgrow(bottomSplitPane, Priority.ALWAYS);
var bottomBox = new VBox(bottomSplitPane); var bottomBox = new VBox(bottomSplitPane);
bottomBox.setAlignment(Pos.CENTER); bottomBox.setAlignment(Pos.CENTER);
var doubleSplitPane = new SplitPane(); var doubleSplitPane = new SplitPane(topBox, bottomBox);
doubleSplitPane.setOrientation(Orientation.VERTICAL); doubleSplitPane.setOrientation(Orientation.VERTICAL);
doubleSplitPane.setDividerPositions(0.5); doubleSplitPane.setDividerPositions(0.5);
doubleSplitPane.getItems().setAll(topBox, bottomBox); //snippet_3:end
doubleSplitPane.setMinSize(400, 200); doubleSplitPane.setMinSize(400, 200);
doubleSplitPane.setMaxSize(400, 200); doubleSplitPane.setMaxSize(400, 200);
var box = new HBox(doubleSplitPane);
return new SampleBlock("Nested", doubleSplitPane); return new ExampleBox(box, new Snippet(getClass(), 3));
} }
private SampleBlock disabledSample() { private ExampleBox multiDividersSplitExample() {
var block = hSplitSample(); //snippet_4:start
block.setTitle("Disabled"); var sp = new SplitPane(
block.getContent().setDisable(true); createBox("First"),
createBox("Second"),
createBox("Third"),
createBox("Fourth")
);
sp.setOrientation(Orientation.HORIZONTAL);
sp.setDividerPositions(0.25, 0.5, 0.75);
//snippet_4:end
return block; sp.setMinSize(600, 200);
sp.setMaxSize(600, 200);
var box = new HBox(sp);
return new ExampleBox(box, new Snippet(getClass(), 4));
} }
private HBox createBox(String text) { private HBox createBox(String s) {
var brick = new HBox(new Text(text)); var label = new Label(s);
brick.setAlignment(Pos.CENTER); label.setMinSize(120, 80);
return brick; label.setAlignment(Pos.CENTER);
label.setStyle("-fx-background-color:-color-accent-subtle;");
var box = new HBox(label);
box.setAlignment(Pos.CENTER);
return box;
} }
} }

@ -2,18 +2,15 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT; import static javafx.scene.control.TabPane.TabClosingPolicy;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
import static javafx.scene.control.TabPane.TabClosingPolicy.UNAVAILABLE;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.List; import java.util.List;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
@ -29,49 +26,219 @@ import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Pane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class TabPanePage extends AbstractPage { public class TabPanePage extends OutlinePage {
public static final String NAME = "TabPane"; public static final String NAME = "TabPane";
private static final double TAB_MIN_HEIGHT = 60;
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;
} }
public TabPanePage() {
super();
addFormattedText("""
[i]TabPane[/i] is a control that provides a container for a group of tabs. By clicking \
on a tab, the content of that tab becomes visible, while the content of the previously \
selected tab gets hidden."""
);
addSection("Usage", usageExample());
addSection("Tab Style", tabStyleExample());
addSection("Vertical Tabs", verticalTabsExample());
addSection("Dense", denseExample());
addSection("Playground", playground());
}
private ExampleBox usageExample() {
//snippet_1:start
var tab1 = new Tab("One");
tab1.setGraphic(new FontIcon(randomIcon()));
var tab2 = new Tab("Two");
tab2.setGraphic(new FontIcon(randomIcon()));
var tab3 = new Tab("Three");
tab3.setGraphic(new FontIcon(randomIcon()));
var tabs = new TabPane(tab1, tab2, tab3);
tabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
tabs.setMinWidth(450);
//snippet_1:end
var box = new HBox(tabs);
box.setMinHeight(50);
var description = BBCodeParser.createFormattedText("""
Tabs are placed within a [font=monospace]TabPane[/font], where each tab represents a single \
"page". Any node such as controls or groups of nodes added to a tab layout container. When \
the user clicks on a tab in the tab content becomes visible."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox tabStyleExample() {
//snippet_2:start
var defaultTabs = new TabPane(
new Tab("One"), new Tab("Two"), new Tab("Three")
);
defaultTabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
defaultTabs.setMinWidth(450);
var floatingTabs = new TabPane(
new Tab("One"), new Tab("Two"), new Tab("Three")
);
floatingTabs.getStyleClass().add(Styles.TABS_FLOATING);
floatingTabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
floatingTabs.setMinWidth(450);
var classicTabs = new TabPane(
new Tab("One"), new Tab("Two"), new Tab("Three")
);
classicTabs.getStyleClass().add(Styles.TABS_CLASSIC);
classicTabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
classicTabs.setMinWidth(450);
//snippet_2:end
var box = new VBox(VGAP_20, defaultTabs, floatingTabs, classicTabs);
box.setMinHeight(200);
var description = BBCodeParser.createFormattedText("""
You can use two additional styles classes to modify tab pane style:
[ul]
[li][code]Styles.TABS_FLOATING[/code][/li]
[li][code]Styles.TABS_CLASSIC[/code][/li][/ul]"""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox verticalTabsExample() {
//snippet_3:start
var defaultTabsLeft = new TabPane(
new Tab("One"), new Tab("Two")
);
defaultTabsLeft.setSide(Side.LEFT);
defaultTabsLeft.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
var floatingTabsLeft = new TabPane(
new Tab("One"), new Tab("Two")
);
floatingTabsLeft.setSide(Side.LEFT);
floatingTabsLeft.getStyleClass().add(Styles.TABS_FLOATING);
floatingTabsLeft.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
var classicTabsLeft = new TabPane(
new Tab("One"), new Tab("Two")
);
classicTabsLeft.setSide(Side.LEFT);
classicTabsLeft.getStyleClass().add(Styles.TABS_CLASSIC);
classicTabsLeft.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
var defaultTabsRight = new TabPane(
new Tab("One"), new Tab("Two")
);
defaultTabsRight.setSide(Side.RIGHT);
defaultTabsRight.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
var floatingTabsRight = new TabPane(
new Tab("One"), new Tab("Two")
);
floatingTabsRight.setSide(Side.RIGHT);
floatingTabsRight.getStyleClass().add(Styles.TABS_FLOATING);
floatingTabsRight.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
var classicTabsRight = new TabPane(
new Tab("One"), new Tab("Two")
);
classicTabsRight.setSide(Side.RIGHT);
classicTabsRight.getStyleClass().add(Styles.TABS_CLASSIC);
classicTabsRight.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
//snippet_3:end
var box = new HBox(
HGAP_20,
defaultTabsLeft, floatingTabsLeft, classicTabsLeft,
new Spacer(),
classicTabsRight, floatingTabsRight, defaultTabsRight
);
box.setMinHeight(450);
var description = BBCodeParser.createFormattedText("""
Tabs in a tab pane can be positioned at any of the four sides by specifying the side."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox denseExample() {
//snippet_4:start
var tab1 = new Tab("One");
tab1.setGraphic(new FontIcon(randomIcon()));
var tab2 = new Tab("Two");
tab2.setGraphic(new FontIcon(randomIcon()));
var tab3 = new Tab("Three");
tab3.setGraphic(new FontIcon(randomIcon()));
var tabs = new TabPane(tab1, tab2, tab3);
tabs.getStyleClass().add(Styles.DENSE);
tabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
tabs.setMinWidth(450);
//snippet_4:end
var box = new HBox(tabs);
box.setMinHeight(50);
var description = BBCodeParser.createFormattedText("""
There's also [code]Styles.DENSE[/code] to make tabs look more compact by cutting label padding."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private Side tabSide = Side.TOP; private Side tabSide = Side.TOP;
private boolean fullWidth = false; private boolean fullWidth = false;
public TabPanePage() { private Pane playground() {
super();
createView();
}
private void createView() {
var tabs = createTabPane(); var tabs = createTabPane();
var tabsLayer = new BorderPane(); var tabsLayer = new BorderPane();
tabsLayer.setTop(tabs); tabsLayer.setTop(tabs);
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> updateTabsWidth(tabsLayer, tabs, fullWidth)); tabs.getTabs().addListener((ListChangeListener<Tab>) c ->
updateTabsWidth(tabsLayer, tabs, fullWidth)
);
var controller = createController(tabsLayer, tabs); var controller = createController(tabsLayer, tabs);
controller.setPrefSize(500, 300);
var controllerLayer = new BorderPane(); var controllerLayer = new BorderPane(controller);
controllerLayer.setCenter(controller); controllerLayer.setMinSize(500, 300);
controllerLayer.setMaxSize(500, 300); controllerLayer.setMaxSize(500, 300);
var root = new StackPane(); var description = BBCodeParser.createFormattedText("""
root.getStyleClass().add(Styles.BORDERED); The playground demonstrates the most important [i]TabPane[/i] features \
root.getChildren().addAll(tabsLayer, controllerLayer); and also serves as an object for monkey testing."""
VBox.setVgrow(root, Priority.ALWAYS); );
setUserContent(new SampleBlock("Playground", root)); var stack = new StackPane(tabsLayer, controllerLayer);
stack.getStyleClass().add(Styles.BORDERED);
stack.setMinSize(600, 500);
return new VBox(VGAP_10, description, stack);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -79,29 +246,31 @@ public class TabPanePage extends AbstractPage {
// == BUTTONS == // == BUTTONS ==
var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP)); var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP));
toTopBtn.getStyleClass().addAll(BUTTON_ICON); toTopBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toTopBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.TOP)); toTopBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.TOP));
var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT)); var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT));
toRightBtn.getStyleClass().addAll(BUTTON_ICON); toRightBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toRightBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.RIGHT)); toRightBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.RIGHT));
var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN)); var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN));
toBottomBtn.getStyleClass().addAll(BUTTON_ICON); toBottomBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toBottomBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.BOTTOM)); toBottomBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.BOTTOM));
var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT)); var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT));
toLeftBtn.getStyleClass().addAll(BUTTON_ICON); toLeftBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toLeftBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.LEFT)); toLeftBtn.setOnAction(e -> rotateTabs(borderPane, tabs, Side.LEFT));
var appendBtn = new Button("", new FontIcon(Feather.PLUS)); var appendBtn = new Button("", new FontIcon(Feather.PLUS));
appendBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT); appendBtn.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.ACCENT);
appendBtn.setOnAction(e -> tabs.getTabs().add(createRandomTab())); appendBtn.setOnAction(e -> tabs.getTabs().add(createRandomTab()));
var buttonsPane = new BorderPane(); var buttonsPane = new BorderPane();
buttonsPane.setMinSize(120, 120); buttonsPane.setMinSize(120, 120);
buttonsPane.setMaxSize(120, 120); buttonsPane.setMaxSize(120, 120);
buttonsPane.setCenter(appendBtn);
buttonsPane.setTop(toTopBtn); buttonsPane.setTop(toTopBtn);
BorderPane.setAlignment(toTopBtn, Pos.CENTER); BorderPane.setAlignment(toTopBtn, Pos.CENTER);
@ -114,16 +283,14 @@ public class TabPanePage extends AbstractPage {
buttonsPane.setLeft(toLeftBtn); buttonsPane.setLeft(toLeftBtn);
BorderPane.setAlignment(toLeftBtn, Pos.CENTER); BorderPane.setAlignment(toLeftBtn, Pos.CENTER);
buttonsPane.setCenter(appendBtn);
// == TOGGLES == // == TOGGLES ==
var closeableToggle = new ToggleSwitch(); var closeableToggle = new ToggleSwitch();
closeableToggle.selectedProperty().addListener((obs, old, val) -> { closeableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val) { if (val) {
tabs.setTabClosingPolicy(ALL_TABS); tabs.setTabClosingPolicy(TabClosingPolicy.ALL_TABS);
} else { } else {
tabs.setTabClosingPolicy(UNAVAILABLE); tabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
} }
}); });
@ -133,7 +300,10 @@ public class TabPanePage extends AbstractPage {
if (val != null && val) { if (val != null && val) {
tabs.setStyle(""); tabs.setStyle("");
} else { } else {
tabs.setStyle("-fx-open-tab-animation:none;-fx-close-tab-animation:none;"); tabs.setStyle("""
-fx-open-tab-animation:none;\
-fx-close-tab-animation:none;"""
);
} }
}); });
@ -146,7 +316,9 @@ public class TabPanePage extends AbstractPage {
}); });
var denseToggle = new ToggleSwitch(); var denseToggle = new ToggleSwitch();
denseToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(tabs, DENSE)); denseToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(tabs, Styles.DENSE)
);
var disableToggle = new ToggleSwitch(); var disableToggle = new ToggleSwitch();
disableToggle.selectedProperty().addListener((obs, old, val) -> { disableToggle.selectedProperty().addListener((obs, old, val) -> {
@ -158,21 +330,11 @@ public class TabPanePage extends AbstractPage {
var togglesGrid = new GridPane(); var togglesGrid = new GridPane();
togglesGrid.setHgap(10); togglesGrid.setHgap(10);
togglesGrid.setVgap(10); togglesGrid.setVgap(10);
togglesGrid.addRow(0, createGridLabel("Closeable"), closeableToggle);
togglesGrid.add(createGridLabel("Closeable"), 0, 0); togglesGrid.addRow(1, createGridLabel("Animated"), animatedToggle);
togglesGrid.add(closeableToggle, 1, 0); togglesGrid.addRow(2, createGridLabel("Full width"), fullWidthToggle);
togglesGrid.addRow(3, createGridLabel("Dense"), denseToggle);
togglesGrid.add(createGridLabel("Animated"), 0, 1); togglesGrid.addRow(4, createGridLabel("Disable"), disableToggle);
togglesGrid.add(animatedToggle, 1, 1);
togglesGrid.add(createGridLabel("Full width"), 0, 2);
togglesGrid.add(fullWidthToggle, 1, 2);
togglesGrid.add(createGridLabel("Dense"), 0, 3);
togglesGrid.add(denseToggle, 1, 3);
togglesGrid.add(createGridLabel("Disable"), 0, 4);
togglesGrid.add(disableToggle, 1, 4);
// == TAB STYLE == // == TAB STYLE ==
@ -180,18 +342,24 @@ public class TabPanePage extends AbstractPage {
var defaultStyleToggle = new ToggleButton("Default"); var defaultStyleToggle = new ToggleButton("Default");
defaultStyleToggle.setToggleGroup(styleToggleGroup); defaultStyleToggle.setToggleGroup(styleToggleGroup);
defaultStyleToggle.setUserData(List.of("whatever", Styles.TABS_FLOATING, Styles.TABS_CLASSIC)); defaultStyleToggle.setUserData(
List.of("whatever", Styles.TABS_FLOATING, Styles.TABS_CLASSIC)
);
defaultStyleToggle.getStyleClass().add(Styles.LEFT_PILL); defaultStyleToggle.getStyleClass().add(Styles.LEFT_PILL);
defaultStyleToggle.setSelected(true); defaultStyleToggle.setSelected(true);
var floatingStyleToggle = new ToggleButton("Floating"); var floatingStyleToggle = new ToggleButton("Floating");
floatingStyleToggle.setToggleGroup(styleToggleGroup); floatingStyleToggle.setToggleGroup(styleToggleGroup);
floatingStyleToggle.setUserData(List.of(Styles.TABS_FLOATING, "whatever", Styles.TABS_CLASSIC)); floatingStyleToggle.setUserData(
List.of(Styles.TABS_FLOATING, "whatever", Styles.TABS_CLASSIC)
);
floatingStyleToggle.getStyleClass().add(Styles.CENTER_PILL); floatingStyleToggle.getStyleClass().add(Styles.CENTER_PILL);
var classicStyleToggle = new ToggleButton("Classic"); var classicStyleToggle = new ToggleButton("Classic");
classicStyleToggle.setToggleGroup(styleToggleGroup); classicStyleToggle.setToggleGroup(styleToggleGroup);
classicStyleToggle.setUserData(List.of(Styles.TABS_CLASSIC, "whatever", Styles.TABS_FLOATING)); classicStyleToggle.setUserData(
List.of(Styles.TABS_CLASSIC, "whatever", Styles.TABS_FLOATING)
);
classicStyleToggle.getStyleClass().add(Styles.RIGHT_PILL); classicStyleToggle.getStyleClass().add(Styles.RIGHT_PILL);
styleToggleGroup.selectedToggleProperty().addListener((obs, old, val) -> { styleToggleGroup.selectedToggleProperty().addListener((obs, old, val) -> {
@ -201,17 +369,13 @@ public class TabPanePage extends AbstractPage {
} }
}); });
var styleBox = new HBox(defaultStyleToggle, floatingStyleToggle, classicStyleToggle); var styleBox = new HBox(defaultStyleToggle, floatingStyleToggle, classicStyleToggle);
styleBox.setAlignment(Pos.CENTER); styleBox.setAlignment(Pos.CENTER);
// == LAYOUT == // == LAYOUT ==
var controls = new HBox(40, var controls = new HBox(
new Spacer(), 40, new Spacer(), buttonsPane, togglesGrid, new Spacer()
buttonsPane,
togglesGrid,
new Spacer()
); );
controls.setAlignment(Pos.CENTER); controls.setAlignment(Pos.CENTER);
@ -256,8 +420,8 @@ public class TabPanePage extends AbstractPage {
private TabPane createTabPane() { private TabPane createTabPane() {
var tabs = new TabPane(); var tabs = new TabPane();
tabs.setTabClosingPolicy(UNAVAILABLE); tabs.setTabClosingPolicy(TabClosingPolicy.UNAVAILABLE);
tabs.setMinHeight(TAB_MIN_HEIGHT); tabs.setMinHeight(60);
// NOTE: Individually disabled tab is still closeable even while it looks // NOTE: Individually disabled tab is still closeable even while it looks
// like disabled. To prevent it from closing one can use "black hole" // like disabled. To prevent it from closing one can use "black hole"

@ -1,331 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BORDERED;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.STRIPED;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.base.theme.Tweaks.ALIGN_CENTER;
import static atlantafx.base.theme.Tweaks.ALIGN_LEFT;
import static atlantafx.base.theme.Tweaks.ALIGN_RIGHT;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static javafx.collections.FXCollections.observableArrayList;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Pos;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.ChoiceBoxTableCell;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.ProgressBarTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.javafx.FontIconTableCell;
public class TablePage extends AbstractPage {
public static final String NAME = "TableView";
@Override
public String getName() {
return NAME;
}
private TableView<Product> table;
private final List<Product> dataList = IntStream.range(1, 51).boxed()
.map(i -> Product.random(i, FAKER))
.toList();
public TablePage() {
super();
var sample = new SampleBlock("Playground", createPlayground());
sample.setFillHeight(true);
setUserContent(sample);
}
private VBox createPlayground() {
// == FOOTER ==
var bordersToggle = new ToggleSwitch("Bordered");
bordersToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(table, BORDERED));
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(table, DENSE));
var stripesToggle = new ToggleSwitch("Striped");
stripesToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(table, STRIPED));
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, value) -> toggleStyleClass(table, Tweaks.EDGE_TO_EDGE)
);
var maxRowCount = 100;
var rowCountChoice = new ComboBox<>(observableArrayList(0, 5, 10, 25, maxRowCount));
rowCountChoice.setValue(maxRowCount);
var rowCountBox = new HBox(BLOCK_HGAP, new Label("rows"), rowCountChoice);
rowCountBox.setAlignment(Pos.CENTER_LEFT);
var footer = new HBox(
BLOCK_HGAP,
new Spacer(),
bordersToggle,
denseToggle,
stripesToggle,
edge2edgeToggle,
new Spacer(),
rowCountBox
);
footer.setAlignment(Pos.CENTER_LEFT);
// == TABLE ==
var filteredData = new FilteredList<>(observableArrayList(dataList));
filteredData.predicateProperty().bind(Bindings.createObjectBinding(
() -> product -> product.getId() <= rowCountChoice.getValue(),
rowCountChoice.valueProperty()
));
var sortedData = new SortedList<>(filteredData);
table = createTable();
table.setItems(sortedData);
sortedData.comparatorProperty().bind(table.comparatorProperty());
VBox.setVgrow(table, Priority.ALWAYS);
// == HEADER ==
var alignGroup = new ToggleGroup();
var alignLeftBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_LEFT));
alignLeftBtn.getStyleClass().add(Styles.LEFT_PILL);
alignLeftBtn.setToggleGroup(alignGroup);
alignLeftBtn.setSelected(true);
alignLeftBtn.setOnAction(e -> {
for (TableColumn<?, ?> c : table.getColumns()) {
c.getStyleClass().removeAll(ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT);
}
});
var alignCenterBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_CENTER));
alignCenterBtn.getStyleClass().add(Styles.CENTER_PILL);
alignCenterBtn.setToggleGroup(alignGroup);
alignCenterBtn.selectedProperty().addListener((obs, old, val) -> {
for (TableColumn<?, ?> c : table.getColumns()) {
addStyleClass(c, ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT);
}
});
var alignRightBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_RIGHT));
alignRightBtn.getStyleClass().add(Styles.RIGHT_PILL);
alignRightBtn.setToggleGroup(alignGroup);
alignRightBtn.selectedProperty().addListener((obs, old, val) -> {
for (TableColumn<?, ?> c : table.getColumns()) {
addStyleClass(c, ALIGN_RIGHT, ALIGN_LEFT, ALIGN_CENTER);
}
});
var alignBox = new HBox(alignLeftBtn, alignCenterBtn, alignRightBtn);
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) {
table.setDisable(val);
}
});
var header = new HBox(
createTablePropertiesMenu(table),
new Spacer(),
alignBox,
new Spacer(),
disableToggle
);
header.setAlignment(Pos.CENTER_LEFT);
// ~
var playground = new VBox(BLOCK_VGAP, header, table, footer);
playground.setMinHeight(100);
return playground;
}
@SuppressWarnings("unchecked")
private TableView<Product> createTable() {
var stateCol = new TableColumn<Product, Boolean>("Selected");
stateCol.setCellValueFactory(new PropertyValueFactory<>("state"));
stateCol.setCellFactory(CheckBoxTableCell.forTableColumn(stateCol));
stateCol.setEditable(true);
// an example of creating index column if data object
// doesn't provide index property
var indexCol = new TableColumn<Product, String>("Index");
indexCol.setCellFactory(col -> {
TableCell<Product, String> cell = new TableCell<>();
StringBinding value = Bindings.when(cell.emptyProperty())
.then("")
.otherwise(cell.indexProperty().add(1).asString());
cell.textProperty().bind(value);
return cell;
});
var iconCol = new TableColumn<Product, Feather>("Logo");
iconCol.setCellValueFactory(c -> new SimpleObjectProperty<>(randomIcon()));
iconCol.setCellFactory(FontIconTableCell.forTableColumn());
iconCol.setEditable(false);
var brandCol = new TableColumn<Product, String>("Brand 🖉");
brandCol.setCellValueFactory(new PropertyValueFactory<>("brand"));
brandCol.setCellFactory(ChoiceBoxTableCell.forTableColumn(
generate(() -> FAKER.commerce().brand(), 10).toArray(String[]::new)
));
brandCol.setEditable(true);
var nameCol = new TableColumn<Product, String>("Name 🖉");
nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
nameCol.setCellFactory(ComboBoxTableCell.forTableColumn(
generate(() -> FAKER.commerce().productName(), 10).toArray(String[]::new)
));
nameCol.setEditable(true);
var priceCol = new TableColumn<Product, String>("Price 🖉");
priceCol.setCellValueFactory(new PropertyValueFactory<>("price"));
priceCol.setCellFactory(TextFieldTableCell.forTableColumn());
priceCol.setEditable(true);
var stockCountCol = new TableColumn<Product, Integer>("Count");
stockCountCol.setCellValueFactory(new PropertyValueFactory<>("count"));
stockCountCol.setEditable(false);
var stockAvailCol = new TableColumn<Product, Double>("Available");
stockAvailCol.setCellValueFactory(new PropertyValueFactory<>("availability"));
stockAvailCol.setCellFactory(ProgressBarTableCell.forTableColumn());
stockAvailCol.setEditable(false);
var stockCol = new TableColumn<Product, Double>("Stock");
stockCol.getColumns().setAll(stockCountCol, stockAvailCol);
var tableView = new TableView<Product>();
tableView.getColumns().setAll(stateCol, indexCol, iconCol, brandCol, nameCol, priceCol, stockCol);
return tableView;
}
private MenuButton createTablePropertiesMenu(TableView<Product> table) {
final var resizePolicyCaption = new CaptionMenuItem("Resize Policy");
final var resizePolicyGroup = new ToggleGroup();
resizePolicyGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof Callback<?, ?> policy) {
//noinspection rawtypes,unchecked
table.setColumnResizePolicy((Callback<TableView.ResizeFeatures, Boolean>) policy);
}
});
final var unconstrainedResizeItem = new RadioMenuItem("Unconstrained");
unconstrainedResizeItem.setToggleGroup(resizePolicyGroup);
unconstrainedResizeItem.setUserData(TableView.UNCONSTRAINED_RESIZE_POLICY);
unconstrainedResizeItem.setSelected(true);
final var constrainedResizeItem = new RadioMenuItem("Constrained");
constrainedResizeItem.setToggleGroup(resizePolicyGroup);
constrainedResizeItem.setUserData(TableView.CONSTRAINED_RESIZE_POLICY);
// ~
final var selectionModeCaption = new CaptionMenuItem("Selection Mode");
final var selectionModeGroup = new ToggleGroup();
selectionModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
table.getSelectionModel().setSelectionMode(mode);
}
});
final var singleSelectionItem = new RadioMenuItem("Single");
singleSelectionItem.setToggleGroup(selectionModeGroup);
singleSelectionItem.setUserData(SelectionMode.SINGLE);
final var multiSelectionItem = new RadioMenuItem("Multiple");
multiSelectionItem.setToggleGroup(selectionModeGroup);
multiSelectionItem.setUserData(SelectionMode.MULTIPLE);
multiSelectionItem.setSelected(true);
// ~
final var editCellsItem = new CheckMenuItem("Editable");
table.editableProperty().bind(editCellsItem.selectedProperty());
editCellsItem.setSelected(true);
final var cellSelectionItem = new CheckMenuItem("Enable cell selection");
table.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelectionItem.selectedProperty());
cellSelectionItem.setSelected(false);
// ~
final var menuButtonItem = new CheckMenuItem("Show menu button");
table.tableMenuButtonVisibleProperty().bind(menuButtonItem.selectedProperty());
menuButtonItem.setSelected(true);
final var propertiesMenu = new MenuButton("Properties");
propertiesMenu.getItems().setAll(
resizePolicyCaption,
unconstrainedResizeItem,
constrainedResizeItem,
selectionModeCaption,
singleSelectionItem,
multiSelectionItem,
new SeparatorMenuItem(),
editCellsItem,
cellSelectionItem,
menuButtonItem
);
return propertiesMenu;
}
private static void addStyleClass(TableColumn<?, ?> c, String styleClass, String... excludes) {
Objects.requireNonNull(c);
Objects.requireNonNull(styleClass);
if (excludes != null && excludes.length > 0) {
c.getStyleClass().removeAll(excludes);
}
c.getStyleClass().add(styleClass);
}
}

@ -0,0 +1,735 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static javafx.collections.FXCollections.observableArrayList;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.Pagination;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.ChoiceBoxTableCell;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.ProgressBarTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIconTableCell;
@SuppressWarnings("unchecked")
public class TableViewPage extends OutlinePage {
public static final String NAME = "TableView";
@Override
public String getName() {
return NAME;
}
public record Book(String author,
String title,
String genre,
String publisher,
SimpleBooleanProperty status) {
public static Book random() {
return new Book(
FAKER.book().author(),
FAKER.book().title(),
FAKER.book().genre(),
FAKER.book().publisher(),
new SimpleBooleanProperty()
);
}
public static ObservableList<Book> random(int count) {
return IntStream.range(0, count).mapToObj(i -> Book.random())
.collect(Collectors.toCollection(FXCollections::observableArrayList));
}
}
public record Flight(String arrival,
String city,
String aircraft,
String airline,
String flight) {
public static Flight random() {
return new Flight(
FAKER.date().past(12, TimeUnit.HOURS, "HH:mm"),
FAKER.country().capital(),
FAKER.aviation().aircraft().toUpperCase(Locale.ROOT),
FAKER.aviation().airline(),
FAKER.aviation().flight()
);
}
public static ObservableList<Flight> random(int count) {
return IntStream.range(0, count).mapToObj(i -> Flight.random())
.sorted(Comparator.comparing(Flight::arrival))
.collect(Collectors.toCollection(FXCollections::observableArrayList));
}
}
///////////////////////////////////////////////////////////////////////////
public TableViewPage() {
super();
addFormattedText("""
The [i]TableView[/i] control is designed to visualize an unlimited number of rows of data, \
broken out into columns."""
);
addSection("Usage", usageExample());
addSection("Row Style", rowStyleExample());
addSection("Selection Color", selectionColorExample());
addSection("Edge-to-Edge", edge2EdgeExample());
addSection("Alignment", alignmentExample());
addSection("Editable", editableExample());
addSection("Column Grouping", columnGroupingExample());
addSection("Pagination", paginationExample());
addSection("Playground", playground());
}
public ExampleBox usageExample() {
//snippet_1:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3, col4);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
//snippet_1:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
var description = BBCodeParser.createFormattedText("""
You can create a table view by instantiating the \
[font=monospace]javafx.scene.control.TableView[/font] class."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox rowStyleExample() {
//snippet_2:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3, col4);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
var borderToggle = new ToggleSwitch("Bordered");
borderToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.BORDERED)
);
var stripeToggle = new ToggleSwitch("Striped");
stripeToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.STRIPED)
);
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.DENSE)
);
//snippet_2:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
HBox.setHgrow(table, Priority.ALWAYS);
var togglesBox = new HBox(HGAP_20, borderToggle, stripeToggle, denseToggle);
togglesBox.setAlignment(Pos.CENTER);
var box = new VBox(VGAP_10, table, togglesBox);
var description = BBCodeParser.createFormattedText("""
The [i]TableView[/i] rows can be styled simply by adding CSS classes:
[ul]
[li][code]Styles.BORDERED[/code] - adds borders between the columns.[/li]
[li][code]Styles.STRIPED[/code] - adds zebra-striping.[/li]
[li][code]Styles.DENSE[/code] - makes the table more compact by cutting cell padding.[/li][/ul]"""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox selectionColorExample() {
var style = """
-color-cell-bg-selected: -color-accent-emphasis;
-color-cell-fg-selected: -color-fg-emphasis;
-color-cell-bg-selected-focused: -color-accent-emphasis;
-color-cell-fg-selected-focused: -color-fg-emphasis;""";
//snippet_3:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3, col4);
// -color-cell-bg-selected: -color-accent-emphasis;
// -color-cell-fg-selected: -color-fg-emphasis;
// -color-cell-bg-selected-focused: -color-accent-emphasis;
// -color-cell-fg-selected-focused: -color-fg-emphasis;
table.setStyle(style);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
//snippet_3:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(250);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
var description = BBCodeParser.createFormattedText("""
Cell selection color (and more) can be changed via looked-up color variables. \
You can find all supported color variables in the docs."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox edge2EdgeExample() {
//snippet_5:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3, col4);
table.getStyleClass().add(Tweaks.EDGE_TO_EDGE);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
//snippet_5:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(250);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
box.setStyle("""
-fx-border-color: -color-accent-emphasis;
-fx-border-width: 2px;"""
);
var description = BBCodeParser.createFormattedText("""
Use [code]Tweaks.EDGE_TO_EDGE[/code] style class to remove the [i]TableView[/i] outer borders. \
This is useful if you want to place the table into external container that already has its \
own borders."""
);
return new ExampleBox(box, new Snippet(getClass(), 5), description);
}
private ExampleBox alignmentExample() {
//snippet_6:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.getStyleClass().add(Tweaks.ALIGN_CENTER);
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.getStyleClass().add(Tweaks.ALIGN_RIGHT);
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.getStyleClass().add(Tweaks.ALIGN_RIGHT);
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.getStyleClass().add(Tweaks.ALIGN_CENTER);
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3, col4);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
//snippet_6:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
var description = BBCodeParser.createFormattedText("""
Column content can be aligned by adding one of the following style class modifiers:
[ul]
[li][code]Tweaks.ALIGN_LEFT[/code] (default)[/li]
[li][code]Tweaks.ALIGN_CENTER[/code][/li]
[li][code]Tweaks.ALIGN_RIGHT[/code][/li][/ul]"""
);
return new ExampleBox(box, new Snippet(getClass(), 6), description);
}
private ExampleBox editableExample() {
//snippet_7:start
var selectAll = new CheckBox();
var col0 = new TableColumn<Book, Boolean>();
col0.setGraphic(selectAll);
col0.setSortable(false);
col0.setCellValueFactory(c -> c.getValue().status());
col0.setCellFactory(CheckBoxTableCell.forTableColumn(col0));
col0.setEditable(true);
var col1 = new TableColumn<Book, String>("Author");
col1.setCellFactory(TextFieldTableCell.forTableColumn());
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().author())
);
var col2 = new TableColumn<Book, String>("Title");
col2.setCellFactory(TextFieldTableCell.forTableColumn());
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().title())
);
var col3 = new TableColumn<Book, String>("Genre");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().genre())
);
col3.setCellFactory(ChoiceBoxTableCell.forTableColumn(
generate(() -> FAKER.book().genre(), 10).toArray(String[]::new)
));
var col4 = new TableColumn<Book, String>("Publisher");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().publisher())
);
col4.setCellFactory(ComboBoxTableCell.forTableColumn(
generate(() -> FAKER.book().publisher(), 10).toArray(String[]::new)
));
var table = new TableView<>(Book.random(5));
table.getColumns().setAll(col0, col1, col2, col3, col4);
table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
selectAll.setOnAction(evt -> {
table.getItems().forEach(
item -> item.status().set(selectAll.isSelected())
);
evt.consume();
});
//snippet_7:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
table.setEditable(true);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
var description = BBCodeParser.createFormattedText(
"the [i]TableView[/i] cells can be made editable by setting an appropriate cell factory."
);
return new ExampleBox(box, new Snippet(getClass(), 7), description);
}
private ExampleBox columnGroupingExample() {
//snippet_8:start
var col11 = new TableColumn<Flight, String>("Time");
col11.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col12 = new TableColumn<Flight, String>("City");
col12.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col1 = new TableColumn<Flight, String>("Arrival");
col1.getColumns().setAll(col11, col12);
var col21 = new TableColumn<Flight, String>("Airline");
col21.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col22 = new TableColumn<Flight, String>("Aircraft");
col22.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().aircraft())
);
var col2 = new TableColumn<Flight, String>("Jet");
col2.getColumns().setAll(col21, col22);
var col3 = new TableColumn<Flight, String>("Flight");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(5));
table.getColumns().setAll(col1, col2, col3);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
//snippet_8:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new HBox(table);
var description = BBCodeParser.createFormattedText(
"Any table column can contain nested columns which allows to group common data properties."
);
return new ExampleBox(box, new Snippet(getClass(), 8), description);
}
private ExampleBox paginationExample() {
//snippet_9:start
var col1 = new TableColumn<Flight, String>("Arrival");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().arrival())
);
var col2 = new TableColumn<Flight, String>("City");
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().city())
);
var col3 = new TableColumn<Flight, String>("Airline");
col3.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().airline())
);
var col4 = new TableColumn<Flight, String>("Flight");
col4.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().flight())
);
var table = new TableView<>(Flight.random(50));
table.getColumns().setAll(col1, col2, col3, col4);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.getSelectionModel().selectFirst();
var pg = new Pagination(25, 0);
pg.setMaxPageIndicatorCount(5);
pg.setPageFactory(pageNum -> {
table.getItems().setAll(Flight.random(50));
return new StackPane(); // null isn't allowed
});
//snippet_9:end
table.setMaxWidth(Double.MAX_VALUE);
table.setMinHeight(300);
HBox.setHgrow(table, Priority.ALWAYS);
var box = new VBox(table, pg);
var description = BBCodeParser.createFormattedText(
"Pagination can be used to split up large amounts of data into smaller chunks."
);
return new ExampleBox(box, new Snippet(getClass(), 9), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private TableView<Product> table;
private final List<Product> dataList = IntStream.range(1, 51).boxed()
.map(i -> atlantafx.sampler.fake.domain.Product.random(i, FAKER))
.toList();
private VBox playground() {
var maxRowCount = 200;
var rowCountChoice = new ComboBox<>(
observableArrayList(0, 1, 5, 10, 25, maxRowCount)
);
rowCountChoice.setValue(5);
var rowCountBox = new HBox(HGAP_20, new Label("rows"), rowCountChoice);
rowCountBox.setAlignment(Pos.CENTER_LEFT);
// == FOOTER ==
var bordersToggle = new ToggleSwitch("Bordered");
bordersToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.BORDERED)
);
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.DENSE)
);
var stripesToggle = new ToggleSwitch("Striped");
stripesToggle.selectedProperty().addListener(
(obs, old, val) -> Styles.toggleStyleClass(table, Styles.STRIPED)
);
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, value) -> Styles.toggleStyleClass(table, Tweaks.EDGE_TO_EDGE)
);
var footer = new HBox(HGAP_20, bordersToggle, denseToggle, stripesToggle, edge2edgeToggle);
footer.setAlignment(Pos.CENTER);
// == TABLE ==
var filteredData = new FilteredList<>(observableArrayList(dataList));
filteredData.predicateProperty().bind(Bindings.createObjectBinding(
() -> product -> product.getId() <= rowCountChoice.getValue(),
rowCountChoice.valueProperty()
));
var sortedData = new SortedList<>(filteredData);
table = createTable();
table.setItems(sortedData);
table.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
sortedData.comparatorProperty().bind(table.comparatorProperty());
VBox.setVgrow(table, Priority.ALWAYS);
// == HEADER ==
var header = new HBox(createTablePropertiesMenu(table), new Spacer(), rowCountBox);
header.setAlignment(Pos.CENTER_LEFT);
// ~
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]TableView[/i] features \
and also serves as an object for monkey testing."""
);
var playground = new VBox(VGAP_10, description, header, table, footer);
playground.setMinHeight(500);
return playground;
}
@SuppressWarnings("unchecked")
private TableView<Product> createTable() {
// an example of creating index column if data object
// doesn't provide index property
var indexCol = new TableColumn<Product, String>("Index");
indexCol.setCellFactory(col -> {
var cell = new TableCell<Product, String>();
StringBinding value = Bindings.when(cell.emptyProperty())
.then("")
.otherwise(cell.indexProperty().add(1).asString());
cell.textProperty().bind(value);
return cell;
});
var iconCol = new TableColumn<Product, Feather>("Logo");
iconCol.setCellValueFactory(c -> new SimpleObjectProperty<>(randomIcon()));
iconCol.setCellFactory(FontIconTableCell.forTableColumn());
var brandCol = new TableColumn<Product, String>("Brand");
brandCol.setCellValueFactory(new PropertyValueFactory<>("brand"));
var nameCol = new TableColumn<Product, String>("Name");
nameCol.setCellValueFactory(new PropertyValueFactory<>("name"));
var priceCol = new TableColumn<Product, String>("Price");
priceCol.setCellValueFactory(new PropertyValueFactory<>("price"));
var stockCountCol = new TableColumn<Product, Integer>("Count");
stockCountCol.setCellValueFactory(new PropertyValueFactory<>("count"));
var stockAvailCol = new TableColumn<Product, Double>("Available");
stockAvailCol.setCellValueFactory(new PropertyValueFactory<>("availability"));
stockAvailCol.setCellFactory(ProgressBarTableCell.forTableColumn());
var stockCol = new TableColumn<Product, Double>("Stock");
stockCol.getColumns().setAll(stockCountCol, stockAvailCol);
var tableView = new TableView<Product>();
tableView.getColumns().setAll(indexCol, iconCol, brandCol, nameCol, priceCol, stockCol);
return tableView;
}
private MenuButton createTablePropertiesMenu(TableView<Product> table) {
final var resizePolCaption = new CaptionMenuItem("Resize Policy");
final var resizePolGroup = new ToggleGroup();
resizePolGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof Callback<?, ?> policy) {
//noinspection rawtypes,unchecked
table.setColumnResizePolicy((Callback<TableView.ResizeFeatures, Boolean>) policy);
}
});
final var unconResizeItem = new RadioMenuItem("Unconstrained");
unconResizeItem.setToggleGroup(resizePolGroup);
unconResizeItem.setUserData(TableView.UNCONSTRAINED_RESIZE_POLICY);
unconResizeItem.setSelected(true);
final var conResizeItem = new RadioMenuItem("Constrained");
conResizeItem.setToggleGroup(resizePolGroup);
conResizeItem.setUserData(TableView.CONSTRAINED_RESIZE_POLICY);
// ~
final var selModeCaption = new CaptionMenuItem("Selection Mode");
final var selModeGroup = new ToggleGroup();
selModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
table.getSelectionModel().setSelectionMode(mode);
}
});
final var singleSelItem = new RadioMenuItem("Single");
singleSelItem.setToggleGroup(selModeGroup);
singleSelItem.setUserData(SelectionMode.SINGLE);
final var multiSelItem = new RadioMenuItem("Multiple");
multiSelItem.setToggleGroup(selModeGroup);
multiSelItem.setUserData(SelectionMode.MULTIPLE);
multiSelItem.setSelected(true);
// ~
final var cellSelItem = new CheckMenuItem("Enable cell selection");
table.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelItem.selectedProperty());
cellSelItem.setSelected(false);
// ~
final var menuBtnItem = new CheckMenuItem("Show menu button");
table.tableMenuButtonVisibleProperty().bind(menuBtnItem.selectedProperty());
menuBtnItem.setSelected(true);
final var propsMenu = new MenuButton("Properties");
propsMenu.getItems().setAll(
resizePolCaption,
unconResizeItem,
conResizeItem,
selModeCaption,
singleSelItem,
multiSelItem,
new SeparatorMenuItem(),
cellSelItem,
menuBtnItem
);
return propsMenu;
}
}

@ -2,21 +2,19 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.STATE_DANGER; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.STATE_SUCCESS; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.Snippet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javafx.scene.control.TextArea; import javafx.scene.control.TextArea;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox;
public class TextAreaPage extends AbstractPage { public class TextAreaPage extends OutlinePage {
public static final String NAME = "TextArea"; public static final String NAME = "TextArea";
private static final double CONTROL_WIDTH = 200;
private static final double CONTROL_HEIGHT = 120;
@Override @Override
public String getName() { public String getName() {
@ -25,68 +23,109 @@ public class TextAreaPage extends AbstractPage {
public TextAreaPage() { public TextAreaPage() {
super(); super();
setUserContent(new FlowPane(
PAGE_HGAP, PAGE_VGAP,
basicSample(),
promptSample(),
scrollSample(),
readonlySample(),
successSample(),
dangerSample(),
disabledSample()
));
}
private SampleBlock basicSample() { addFormattedText(
var textArea = createTextArea("Text"); "Text input component that allows a user to enter multiple lines of plain text."
textArea.setWrapText(true);
return new SampleBlock("Basic", textArea);
}
private SampleBlock promptSample() {
var textArea = createTextArea(null);
textArea.setPromptText("Prompt text");
return new SampleBlock("Prompt", textArea);
}
private SampleBlock scrollSample() {
var textArea = createTextArea(
Stream.generate(() -> FAKER.lorem().paragraph()).limit(10).collect(Collectors.joining("\n"))
); );
textArea.setWrapText(false); addSection("Usage", usageExample());
return new SampleBlock("Scrolling", textArea); addSection("Prompt Text", promptTextExample());
addSection("Readonly", readonlyExample());
addSection("Color", colorExample());
} }
private SampleBlock readonlySample() { private ExampleBox usageExample() {
var textArea = createTextArea("Text"); //snippet_1:start
textArea.setEditable(false); var ta1 = new TextArea("Text");
return new SampleBlock("Readonly", textArea);
var ta2 = new TextArea(
Stream.generate(() -> FAKER.lorem().paragraph())
.limit(10)
.collect(Collectors.joining("\n"))
);
ta2.setWrapText(true);
//snippet_1:end
ta1.setMinSize(300, 120);
ta1.setMaxSize(300, 120);
ta2.setMinSize(300, 120);
ta2.setMaxSize(300, 120);
var box = new HBox(HGAP_20, ta1, ta2);
var description = BBCodeParser.createFormattedText("""
You create a text area control by creating an instance of the \
[font=monospace]javafx.scene.control.TextArea[/font] class. \
By default long text won't be wrapped. You should set [code]setWrapText(true)[/code] \
to use this feature."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock successSample() { private ExampleBox promptTextExample() {
var textArea = createTextArea("Text"); //snippet_2:start
textArea.pseudoClassStateChanged(STATE_SUCCESS, true); var ta = new TextArea();
return new SampleBlock("Success", textArea); ta.setPromptText("Prompt text");
ta.setWrapText(true);
//snippet_2:end
ta.setMaxSize(300, 120);
ta.setMinSize(300, 120);
var box = new HBox(ta);
var description = BBCodeParser.createFormattedText("""
The [i]TextArea[/i] supports the notion of showing prompt text to the user when there \
is no text already in the text area (either via the user, or set programmatically). \
This is a useful way of informing the user as to what is expected in the [i]TextArea[/i], \
without having to resort to tooltips or on-screen labels."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
private SampleBlock dangerSample() { private ExampleBox readonlyExample() {
var textArea = createTextArea("Text"); //snippet_3:start
textArea.pseudoClassStateChanged(STATE_DANGER, true); var ta = new TextArea("This text can't be modified");
return new SampleBlock("Danger", textArea); ta.setWrapText(true);
ta.setEditable(false);
//snippet_3:end
ta.setMaxSize(300, 120);
ta.setMinSize(300, 120);
var box = new HBox(ta);
var description = BBCodeParser.createFormattedText("""
The [i]TextArea[/i]'s [code]editable[/code] property indicates whether it \
can be edited by the user."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
} }
private SampleBlock disabledSample() { private ExampleBox colorExample() {
var textArea = createTextArea("Text"); //snippet_4:start
textArea.setDisable(true); var ta1 = new TextArea("Text");
return new SampleBlock("Disabled", textArea); ta1.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
} ta1.setWrapText(true);
private TextArea createTextArea(String text) { var ta2 = new TextArea("Text");
var textArea = new TextArea(text); ta2.pseudoClassStateChanged(Styles.STATE_DANGER, true);
textArea.setMinWidth(CONTROL_WIDTH); ta2.setWrapText(true);
textArea.setMinHeight(CONTROL_HEIGHT); //snippet_4:end
textArea.setMaxWidth(CONTROL_WIDTH);
textArea.setMaxHeight(CONTROL_HEIGHT); ta1.setMaxSize(300, 120);
return textArea; ta1.setMinSize(300, 120);
ta2.setMaxSize(300, 120);
ta2.setMinSize(300, 120);
var box = new HBox(HGAP_20, ta1, ta2);
var description = BBCodeParser.createFormattedText("""
You can use [code]Styles.STATE_SUCCESS[/code] or [code]Styles.STATE_DANGER[/code] \
pseudo-classes to change the [i]TextArea[/i] color. This especially useful to indicate \
the validation result."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
} }
} }

@ -2,22 +2,17 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.LARGE; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.ROUNDED; import atlantafx.base.util.BBCodeParser;
import static atlantafx.base.theme.Styles.SMALL; import atlantafx.sampler.page.ExampleBox;
import static atlantafx.base.theme.Styles.STATE_DANGER; import atlantafx.sampler.page.OutlinePage;
import static atlantafx.base.theme.Styles.STATE_SUCCESS; import atlantafx.sampler.page.Snippet;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.PasswordField; import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class TextFieldPage extends AbstractPage { public class TextFieldPage extends OutlinePage {
public static final String NAME = "TextField"; public static final String NAME = "TextField";
@ -28,81 +23,165 @@ public class TextFieldPage extends AbstractPage {
public TextFieldPage() { public TextFieldPage() {
super(); super();
setUserContent(new VBox(
PAGE_VGAP, addFormattedText("""
expandingHBox(basicSample(), promptSample(), passwordSample()), Text input component that allows a user to enter a single line of unformatted text."""
expandingHBox(readonlySample(), successSample(), dangerSample()), );
expandingHBox(sizeSample(), roundedSample()), addSection("Usage", usageExample());
disabledSample() addSection("Prompt Text", promptTextExample());
)); addSection("Readonly", readonlyExample());
addSection("Color", colorExample());
addSection("Password Field", passwordFieldExample());
addSection("Size", sizeExample());
addSection("Rounded", roundedExample());
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
var field = new TextField("Text"); //snippet_1:start
return new SampleBlock("Basic", field); var tf1 = new TextField("Text");
var tf2 = new TextField(FAKER.lorem().sentence(20));
//snippet_1:end
tf1.setPrefWidth(200);
tf2.setPrefWidth(200);
var box = new HBox(HGAP_20, tf1, tf2);
var description = BBCodeParser.createFormattedText("""
You create a text field control by creating an instance of the \
[font=monospace]javafx.scene.control.TextField[/font] class."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock passwordSample() { private ExampleBox promptTextExample() {
var field = new PasswordField(); //snippet_2:start
field.setText("qwerty"); var tf = new TextField();
return new SampleBlock("Password", field); tf.setPromptText("Prompt text");
//snippet_2:end
tf.setPrefWidth(200);
var box = new HBox(tf);
var description = BBCodeParser.createFormattedText("""
The [i]TextField[/i] supports the notion of showing prompt text to the user when there \
is no text already in the text field (either via the user, or set programmatically). \
This is a useful way of informing the user as to what is expected in the text field, \
without having to resort to tooltips or on-screen labels."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
private SampleBlock promptSample() { private ExampleBox readonlyExample() {
var field = new TextField(); //snippet_3:start
field.setPromptText("Prompt text"); var tf = new TextField("This text can't be modified");
return new SampleBlock("Prompt", field); tf.setEditable(false);
//snippet_3:end
tf.setPrefWidth(200);
var box = new HBox(tf);
var description = BBCodeParser.createFormattedText("""
[i]TextField[/i]'s [code]editable[/code] property indicates whether it can be edited by the user."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
} }
private SampleBlock readonlySample() { private ExampleBox colorExample() {
var field = new TextField("Text"); //snippet_4:start
field.setEditable(false); var tf1 = new TextField("Text");
return new SampleBlock("Readonly", field); tf1.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
var tf2 = new TextField("Text");
tf2.pseudoClassStateChanged(Styles.STATE_DANGER, true);
//snippet_4:end
tf1.setPrefWidth(200);
tf2.setPrefWidth(200);
var box = new HBox(HGAP_20, tf1, tf2);
var description = BBCodeParser.createFormattedText("""
You can use [code]Styles.STATE_SUCCESS[/code] or [code]Styles.STATE_DANGER[/code] \
pseudo-classes to change the [i]TextField[/i] color. This especially useful to indicate \
the validation result."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
} }
private SampleBlock successSample() { private ExampleBox passwordFieldExample() {
var field = new TextField("Text"); //snippet_5:start
field.pseudoClassStateChanged(STATE_SUCCESS, true); var tf = new PasswordField();
return new SampleBlock("Success", field); tf.setText("qwerty");
//snippet_5:end
tf.setPrefWidth(200);
var box = new HBox(tf);
var description = BBCodeParser.createFormattedText("""
The [i]TextField[/i] flavor that masks entered characters. Note that password can \
not be revealed. If you need this particular feature try [code]PasswordTextField[/code]. \
It's the [code]CustomTextField[/code] flavor that does the same thing."""
);
return new ExampleBox(box, new Snippet(getClass(), 5), description);
} }
private SampleBlock dangerSample() { private ExampleBox sizeExample() {
var field = new TextField("Text"); //snippet_6:start
field.pseudoClassStateChanged(STATE_DANGER, true);
return new SampleBlock("Danger", field);
}
private SampleBlock sizeSample() {
var smallField = new TextField("Small"); var smallField = new TextField("Small");
smallField.getStyleClass().add(SMALL); smallField.getStyleClass().add(Styles.SMALL);
smallField.setPrefWidth(70);
var normalField = new TextField("Normal"); var normalField = new TextField("Normal");
normalField.setPrefWidth(120);
var largeField = new TextField("Large"); var largeField = new TextField("Large");
largeField.getStyleClass().add(LARGE); largeField.getStyleClass().add(Styles.LARGE);
//snippet_6:end
smallField.setPrefWidth(70);
normalField.setPrefWidth(120);
largeField.setPrefWidth(200); largeField.setPrefWidth(200);
var content = new HBox(BLOCK_HGAP, smallField, normalField, largeField); var box = new HBox(HGAP_20, smallField, normalField, largeField);
content.setAlignment(Pos.CENTER_LEFT); box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Size", content); var description = BBCodeParser.createFormattedText("""
Use [code]Styles.SMALL[/code] and [code]Styles.LARGE[/code] style classes \
to change the [i]TextField[/i] size."""
);
return new ExampleBox(box, new Snippet(getClass(), 6), description);
} }
private SampleBlock roundedSample() { private ExampleBox roundedExample() {
var field = new TextField("Text"); //snippet_7:start
field.getStyleClass().add(ROUNDED); var smallField = new TextField("Small");
return new SampleBlock("Rounded", field); smallField.getStyleClass().addAll(
} Styles.SMALL, Styles.ROUNDED
);
private SampleBlock disabledSample() { var normalField = new TextField("Normal");
var field = new TextField("Text"); normalField.getStyleClass().addAll(Styles.ROUNDED);
field.setDisable(true);
var block = new SampleBlock("Disabled", field); var largeField = new TextField("Large");
block.setMaxWidth(250); largeField.getStyleClass().addAll(
Styles.LARGE, Styles.ROUNDED
);
//snippet_7:end
return block; smallField.setPrefWidth(70);
normalField.setPrefWidth(120);
largeField.setPrefWidth(200);
var box = new HBox(HGAP_20, smallField, normalField, largeField);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
Use [code]Styles.ROUNDED[/code] style class to round the [i]TextField[/i] corners."""
);
return new ExampleBox(box, new Snippet(getClass(), 7), description);
} }
} }

@ -2,26 +2,23 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.ELEVATED_2;
import static atlantafx.base.theme.Styles.INTERACTIVE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static javafx.geometry.HPos.RIGHT; import static javafx.geometry.HPos.RIGHT;
import static javafx.scene.layout.Priority.NEVER; import static javafx.scene.layout.Priority.NEVER;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.Page; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import javafx.scene.control.TitledPane; import javafx.scene.control.TitledPane;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
@ -30,10 +27,9 @@ import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
public class TitledPanePage extends AbstractPage { public class TitledPanePage extends OutlinePage {
public static final String NAME = "TitledPane"; public static final String NAME = "TitledPane";
private static final String ELEVATED_PREFIX = "elevated-";
@Override @Override
public String getName() { public String getName() {
@ -42,36 +38,160 @@ public class TitledPanePage extends AbstractPage {
public TitledPanePage() { public TitledPanePage() {
super(); super();
createView();
}
private void createView() { addFormattedText("""
var samples = new HBox( [i]TitledPane[/i] is a panel with a title that can be opened and closed. \
PAGE_HGAP, It holds one or more user interface elements and you can expand and collapse it. \
interactivePane(), Some of the [i]TitledPane[/i] can be applied to the [i]Accordion[/i] as well."""
disabledPane(),
untitledPane()
); );
samples.getChildren().forEach(c -> ((TitledPane) c).setPrefSize(500, 120)); addSection("Usage", usageExample());
addSection("Elevation", elevationExample());
addSection("Dense", denseExample());
addSection("Alternative Icon", altIconExample());
addSection("Playground", playground());
setUserContent(new VBox( var dummyBox = new HBox();
Page.PAGE_VGAP, dummyBox.setMinHeight(10);
createPlayground(), addNode(dummyBox);
samples
));
} }
private TitledPane createPlayground() { private ExampleBox usageExample() {
var playground = new TitledPane(); //snippet_1:start
playground.setText("_Playground"); var tp1 = new TitledPane(
playground.setMnemonicParsing(true); "Header",
playground.getStyleClass().add(ELEVATED_2); new Label("Content")
);
var textFlow = new TextFlow(new Text(FAKER.lorem().paragraph(10))); var tp2 = new TitledPane("Header", new Label("Content"));
tp2.setCollapsible(false);
//snippet_1:end
tp1.setPrefWidth(200);
tp2.setPrefWidth(200);
var box = new HBox(HGAP_20, tp1, tp2);
var description = BBCodeParser.createFormattedText("""
The panel in a [i]TitledPane[/i] can be any node such as controls or groups of nodes \
added to a layout container. Note that whilst [i]TitledPane[/i] extends from [i]Labeled[/i], \
the inherited properties are used to manipulate the [i]TitledPane[/i] header, not the \
content area itself."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox elevationExample() {
//snippet_2:start
var tp0 = new TitledPane(
"Default",
new Label("Content")
);
var tp1 = new TitledPane(
"Styles.ELEVATED_1",
new Label("Content")
);
tp1.getStyleClass().add(Styles.ELEVATED_1);
var tp2 = new TitledPane(
"Styles.ELEVATED_2",
new Label("Content")
);
tp2.getStyleClass().add(Styles.ELEVATED_2);
var tp3 = new TitledPane(
"Styles.ELEVATED_3",
new Label("Content")
);
tp3.getStyleClass().add(Styles.ELEVATED_3);
var tp4 = new TitledPane(
"Styles.ELEVATED_4",
new Label("Content")
);
tp4.getStyleClass().add(Styles.ELEVATED_4);
var tp5 = new TitledPane(
"Styles.INTERACTIVE",
new Label("Hover here")
);
tp5.getStyleClass().add(Styles.INTERACTIVE);
//snippet_2:end
tp0.setPrefWidth(300);
tp1.setPrefWidth(300);
tp2.setPrefWidth(300);
tp3.setPrefWidth(300);
tp4.setPrefWidth(300);
tp5.setPrefWidth(300);
var box = new FlowPane(HGAP_30, VGAP_20, tp0, tp1, tp2, tp3, tp4, tp5);
var description = BBCodeParser.createFormattedText("""
With [code]Styles.ELEVATED_N[/code] or [code]Styles.INTERACTIVE[/code] styles classes \
you can add raised shadow effect to the [i]TitledPane[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox denseExample() {
//snippet_3:start
var tp = new TitledPane(
"Header",
new Label("Content")
);
tp.getStyleClass().add(Styles.DENSE);
//snippet_3:end
tp.setPrefWidth(200);
var box = new HBox(tp);
var description = BBCodeParser.createFormattedText("""
If you need more compact view there's [code]Styles.DENSE[/code] for that."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox altIconExample() {
//snippet_3:start
var tp = new TitledPane(
"Header",
new Label("Content")
);
tp.getStyleClass().add(Tweaks.ALT_ICON);
//snippet_3:end
tp.setPrefWidth(200);
var box = new HBox(tp);
var description = BBCodeParser.createFormattedText("""
There's additional tweak [code]Tweaks.ALT_ICON[/code] to change header \
arrow icon to the classic style."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private VBox playground() {
var titledPane = new TitledPane();
titledPane.setText("_Header");
titledPane.setMnemonicParsing(true);
titledPane.getStyleClass().add(Styles.ELEVATED_2);
var textFlow = new TextFlow(new Text(
FAKER.lorem().paragraph(10))
);
textFlow.setMinHeight(Region.USE_PREF_SIZE); textFlow.setMinHeight(Region.USE_PREF_SIZE);
textFlow.setMaxHeight(Region.USE_PREF_SIZE); textFlow.setMaxHeight(Region.USE_PREF_SIZE);
VBox.setVgrow(textFlow, Priority.ALWAYS);
textFlow.setLineSpacing(5); textFlow.setLineSpacing(5);
String elevationPrefix = "elevated-";
var elevationSlider = new Slider(0, 4, 2); var elevationSlider = new Slider(0, 4, 2);
elevationSlider.setShowTickLabels(true); elevationSlider.setShowTickLabels(true);
elevationSlider.setShowTickMarks(true); elevationSlider.setShowTickMarks(true);
@ -82,15 +202,17 @@ public class TitledPanePage extends AbstractPage {
elevationSlider.setMinWidth(150); elevationSlider.setMinWidth(150);
elevationSlider.setMaxWidth(150); elevationSlider.setMaxWidth(150);
elevationSlider.valueProperty().addListener((obs, old, val) -> { elevationSlider.valueProperty().addListener((obs, old, val) -> {
playground.getStyleClass().removeAll( titledPane.getStyleClass().removeAll(
playground.getStyleClass().stream().filter(c -> c.startsWith(ELEVATED_PREFIX)).toList() titledPane.getStyleClass().stream()
.filter(c -> c.startsWith(elevationPrefix))
.toList()
); );
if (val == null) { if (val == null) {
return; return;
} }
int level = val.intValue(); int level = val.intValue();
if (level > 0) { if (level > 0) {
playground.getStyleClass().add(ELEVATED_PREFIX + level); titledPane.getStyleClass().add(elevationPrefix + level);
} }
}); });
@ -99,65 +221,46 @@ public class TitledPanePage extends AbstractPage {
// for some reason it still preserves arrow button gap. #javafx-bug // for some reason it still preserves arrow button gap. #javafx-bug
var collapseToggle = new ToggleSwitch("Collapsible"); var collapseToggle = new ToggleSwitch("Collapsible");
collapseToggle.setSelected(true); collapseToggle.setSelected(true);
playground.collapsibleProperty().bind(collapseToggle.selectedProperty()); titledPane.collapsibleProperty().bind(collapseToggle.selectedProperty());
var animateToggle = new ToggleSwitch("Animated"); var animateToggle = new ToggleSwitch("Animated");
animateToggle.setSelected(true); animateToggle.setSelected(true);
playground.animatedProperty().bind(animateToggle.selectedProperty()); titledPane.animatedProperty().bind(animateToggle.selectedProperty());
var denseToggle = new ToggleSwitch("Dense"); var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(playground, DENSE)); denseToggle.selectedProperty()
.addListener((obs, old, val) -> Styles.toggleStyleClass(titledPane, Styles.DENSE));
var altIconToggle = new ToggleSwitch("Alt icon"); var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(playground, Tweaks.ALT_ICON)); altIconToggle.selectedProperty()
.addListener((obs, old, val) -> Styles.toggleStyleClass(titledPane, Tweaks.ALT_ICON));
var toggles = new GridPane(); var toggles = new GridPane();
toggles.setVgap(10); toggles.setHgap(HGAP_20);
toggles.setHgap(BLOCK_HGAP); toggles.setVgap(VGAP_10);
toggles.getColumnConstraints().setAll( toggles.getColumnConstraints().setAll(
new ColumnConstraints(-1, -1, -1, NEVER, RIGHT, false), new ColumnConstraints(-1, -1, -1, NEVER, RIGHT, false),
new ColumnConstraints(-1, -1, -1, NEVER, RIGHT, false) new ColumnConstraints(-1, -1, -1, NEVER, RIGHT, false)
); );
toggles.add(collapseToggle, 0, 0); toggles.addRow(0, collapseToggle, animateToggle);
toggles.add(animateToggle, 1, 0); toggles.addRow(1, denseToggle, altIconToggle);
toggles.add(denseToggle, 0, 1);
toggles.add(altIconToggle, 1, 1);
var controls = new HBox(BLOCK_HGAP); var elevationBox = new HBox(10, new Label("Elevation"), elevationSlider);
controls.setMinHeight(80);
var controls = new HBox(VGAP_10);
controls.setMinHeight(100);
controls.setFillHeight(false); controls.setFillHeight(false);
controls.setAlignment(Pos.CENTER_LEFT); controls.setAlignment(Pos.CENTER_LEFT);
controls.getChildren().setAll( controls.getChildren().setAll(elevationBox, new Spacer(), toggles);
new Label("Elevation"),
elevationSlider, var content = new VBox(VGAP_10, textFlow, controls);
new Spacer(), titledPane.setContent(content);
toggles
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]TitledPane[/i] features \
and also serves as an object for monkey testing."""
); );
var content = new VBox(BLOCK_VGAP, textFlow, controls); return new VBox(VGAP_10, description, titledPane);
VBox.setVgrow(textFlow, Priority.ALWAYS);
playground.setContent(content);
return playground;
}
private TitledPane interactivePane() {
var titledPane = new TitledPane("Interactive", new Text("Hover here."));
titledPane.setCollapsible(false);
titledPane.getStyleClass().add(INTERACTIVE);
return titledPane;
}
private TitledPane disabledPane() {
var titledPane = new TitledPane("Disabled", new CheckBox("This checkbox is disabled."));
titledPane.setCollapsible(false);
titledPane.setDisable(true);
return titledPane;
}
private TitledPane untitledPane() {
var titledPane = new TitledPane("This pane has no title.", new Text());
titledPane.setCollapsible(false);
return titledPane;
} }
} }

@ -2,26 +2,21 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import atlantafx.base.theme.Styles;
import static atlantafx.base.theme.Styles.CENTER_PILL; import atlantafx.base.util.BBCodeParser;
import static atlantafx.base.theme.Styles.LEFT_PILL; import atlantafx.sampler.page.ExampleBox;
import static atlantafx.base.theme.Styles.RIGHT_PILL; import atlantafx.sampler.page.OutlinePage;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP; import atlantafx.sampler.page.Snippet;
import static atlantafx.sampler.util.Controls.toggleButton; import javafx.geometry.Pos;
import static javafx.scene.layout.GridPane.REMAINING;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public class ToggleButtonPage extends AbstractPage { public class ToggleButtonPage extends OutlinePage {
public static final String NAME = "ToggleButton"; public static final String NAME = "ToggleButton";
@ -32,103 +27,158 @@ public class ToggleButtonPage extends AbstractPage {
public ToggleButtonPage() { public ToggleButtonPage() {
super(); super();
createView();
addFormattedText("""
A [i]ToggleButton[/i] is a special control having the ability to be selected."""
);
addSection("Usage", usageExample());
addSection("Icon Only", iconOnlyExample());
addSection("Segmented Group", segmentedGroupExample());
} }
private void createView() { private ExampleBox usageExample() {
var grid = new GridPane(); //snippet_1:start
grid.setHgap(Page.PAGE_HGAP); var singleBtn = new ToggleButton("Toggle");
grid.setVgap(Page.PAGE_VGAP); singleBtn.setSelected(true);
grid.add(basicSample(), 0, 0, REMAINING, 1); // ~
grid.add(wizardSample(), 0, 1);
grid.add(iconOnlySample(), 1, 1);
grid.add(disabledSample(), 0, 2);
setUserContent(grid); var leftBtn1 = new ToggleButton("._left-pill");
} leftBtn1.getStyleClass().add(Styles.LEFT_PILL);
leftBtn1.setSelected(true);
private SampleBlock basicSample() { var rightBtn1 = new ToggleButton("._right-pill");
var threeButtonGroup = new ToggleGroup(); rightBtn1.getStyleClass().add(Styles.RIGHT_PILL);
var leftPill = toggleButton("._left-pill", null, threeButtonGroup, true, LEFT_PILL); var twoBtnGroup = new ToggleGroup();
leftPill.setMnemonicParsing(true); twoBtnGroup.getToggles().addAll(leftBtn1, rightBtn1);
var centerPill = toggleButton("._center-pill", null, threeButtonGroup, false, CENTER_PILL); // ~
centerPill.setMnemonicParsing(true);
var rightPill = toggleButton("._right-pill", null, threeButtonGroup, false, RIGHT_PILL); var leftBtn2 = new ToggleButton("._left-pill");
rightPill.setMnemonicParsing(true); leftBtn2.getStyleClass().add(Styles.LEFT_PILL);
leftBtn2.setMnemonicParsing(true);
leftBtn2.setSelected(true);
var threeButtonBox = new HBox(leftPill, centerPill, rightPill); var centerBtn2 = new ToggleButton("._center-pill");
centerBtn2.getStyleClass().add(Styles.CENTER_PILL);
centerBtn2.setMnemonicParsing(true);
var twoButtonGroup = new ToggleGroup(); var rightBtn2 = new ToggleButton("._right-pill");
var twoButtonBox = new HBox( rightBtn2.getStyleClass().add(Styles.RIGHT_PILL);
toggleButton(".left-pill", null, twoButtonGroup, true, LEFT_PILL), rightBtn2.setMnemonicParsing(true);
toggleButton(".right-pill", null, twoButtonGroup, false, RIGHT_PILL)
var threeBtnGroup = new ToggleGroup();
threeBtnGroup.getToggles().addAll(leftBtn2, centerBtn2, rightBtn2);
//snippet_1:end
var twoBtnBox = new HBox(leftBtn1, rightBtn1);
twoBtnBox.setAlignment(Pos.CENTER_LEFT);
var threeBtnBox = new HBox(leftBtn2, centerBtn2, rightBtn2);
threeBtnBox.setAlignment(Pos.CENTER_LEFT);
var box = new HBox(HGAP_30, singleBtn, twoBtnBox, threeBtnBox);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The [i]ToggleButtons[/i] can also be placed in groups. When in groups, only one button \
at a time within that group can be selected. Use [code]Styles.*_PILL[/code] style \
classes if you want to toggle group to look like a single "segmented" button."""
); );
return new SampleBlock("Basic", new HBox(BLOCK_HGAP, threeButtonBox, twoButtonBox)); return new ExampleBox(box, new Snippet(getClass(), 1), description);
} }
private SampleBlock wizardSample() { private ExampleBox iconOnlyExample() {
//snippet_2:start
var leftBtn = new ToggleButton("", new FontIcon(Feather.BOLD));
leftBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.LEFT_PILL
);
leftBtn.setSelected(true);
var centerBtn = new ToggleButton("", new FontIcon(Feather.ITALIC));
centerBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.CENTER_PILL
);
var rightBtn = new ToggleButton("", new FontIcon(Feather.UNDERLINE));
rightBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.RIGHT_PILL
);
//snippet_2:end
var box = new HBox(leftBtn, centerBtn, rightBtn);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
Sometimes it's desirable to create a button group that consists from icon \
buttons only. This is also supported."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox segmentedGroupExample() {
//snippet_3:start
var musicBtn = new ToggleButton(
"Music", new FontIcon(Feather.MUSIC)
);
musicBtn.getStyleClass().add(Styles.LEFT_PILL);
musicBtn.setSelected(true);
var imagesBtn = new ToggleButton(
"Images", new FontIcon(Feather.IMAGE)
);
imagesBtn.getStyleClass().add(Styles.CENTER_PILL);
var videosBtn = new ToggleButton(
"Videos", new FontIcon(Feather.VIDEO)
);
videosBtn.getStyleClass().add(Styles.RIGHT_PILL);
var group = new ToggleGroup(); var group = new ToggleGroup();
group.getToggles().addAll(musicBtn, imagesBtn, videosBtn);
var prevBtn = new Button("\f", new FontIcon(Feather.CHEVRON_LEFT));
prevBtn.getStyleClass().addAll(BUTTON_ICON, LEFT_PILL, "toggle-button");
prevBtn.setOnAction(e -> {
int selected = group.getToggles().indexOf(group.getSelectedToggle());
if (selected > 0) {
group.selectToggle(group.getToggles().get(selected - 1));
}
});
var nextBtn = new Button("\f", new FontIcon(Feather.CHEVRON_RIGHT));
nextBtn.getStyleClass().addAll(BUTTON_ICON, RIGHT_PILL, "toggle-button");
nextBtn.setContentDisplay(ContentDisplay.RIGHT);
nextBtn.setOnAction(e -> {
int selected = group.getToggles().indexOf(group.getSelectedToggle());
if (selected < group.getToggles().size() - 1) {
group.selectToggle(group.getToggles().get(selected + 1));
}
});
var wizard = new HBox(
prevBtn,
toggleButton("Music", Feather.MUSIC, group, true, CENTER_PILL),
toggleButton("Images", Feather.IMAGE, group, false, CENTER_PILL),
toggleButton("Videos", Feather.VIDEO, group, false, CENTER_PILL),
nextBtn
);
group.selectedToggleProperty().addListener((obs, old, val) -> { group.selectedToggleProperty().addListener((obs, old, val) -> {
if (val == null) { if (val == null) {
old.setSelected(true); old.setSelected(true);
} }
}); });
return new SampleBlock("Wizard", wizard); var prevBtn = new Button("\f", new FontIcon(Feather.CHEVRON_LEFT));
} prevBtn.getStyleClass().addAll(
Styles.BUTTON_ICON, Styles.LEFT_PILL, "toggle-button"
);
prevBtn.setOnAction(evt -> {
int sel = group.getToggles().indexOf(group.getSelectedToggle());
if (sel > 0) {
group.selectToggle(group.getToggles().get(sel - 1));
}
});
private SampleBlock iconOnlySample() { var nextBtn = new Button("\f", new FontIcon(Feather.CHEVRON_RIGHT));
var icons = new HBox( nextBtn.getStyleClass().addAll(
toggleButton("", Feather.BOLD, null, true, BUTTON_ICON, LEFT_PILL), Styles.BUTTON_ICON, Styles.RIGHT_PILL, "toggle-button"
toggleButton("", Feather.ITALIC, null, false, BUTTON_ICON, CENTER_PILL), );
toggleButton("", Feather.UNDERLINE, null, false, BUTTON_ICON, RIGHT_PILL) nextBtn.setContentDisplay(ContentDisplay.RIGHT);
nextBtn.setOnAction(evt -> {
int sel = group.getToggles().indexOf(group.getSelectedToggle());
if (sel < group.getToggles().size() - 1) {
group.selectToggle(group.getToggles().get(sel + 1));
}
});
var groupBox = new HBox(
prevBtn, musicBtn, imagesBtn, videosBtn, nextBtn
);
groupBox.setAlignment(Pos.CENTER_LEFT);
//snippet_3:end
var description = BBCodeParser.createFormattedText("""
This example demonstrates how to create a complex segmented button group."""
); );
return new SampleBlock("Icon only", icons); return new ExampleBox(groupBox, new Snippet(getClass(), 3), description);
}
private SampleBlock disabledSample() {
var group = new ToggleGroup();
var content = new HBox(
toggleButton(".left-pill", null, group, false, LEFT_PILL),
toggleButton(".center-pill", null, group, false, CENTER_PILL),
toggleButton(".right-pill", null, group, true, RIGHT_PILL)
);
content.getChildren().get(0).setDisable(true);
content.getChildren().get(1).setDisable(true);
return new SampleBlock("Disabled", content);
} }
} }

@ -1,70 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.HorizontalDirection;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.VBox;
public class ToggleSwitchPage extends AbstractPage {
public static final String NAME = "ToggleSwitch";
@Override
public String getName() {
return NAME;
}
public ToggleSwitchPage() {
super();
setUserContent(new FlowPane(
Page.PAGE_HGAP, Page.PAGE_VGAP,
basicSample(),
stateSample()
));
}
private SampleBlock basicSample() {
var leftToggle = new ToggleSwitch("Enabled");
leftToggle.selectedProperty().addListener(
(obs, old, val) -> leftToggle.setText(val ? "Enabled" : "Disabled")
);
leftToggle.setSelected(true);
var rightToggle = new ToggleSwitch("Disabled");
rightToggle.selectedProperty().addListener(
(obs, old, val) -> rightToggle.setText(val ? "Enabled" : "Disabled")
);
rightToggle.setLabelPosition(HorizontalDirection.RIGHT);
rightToggle.setSelected(false);
return new SampleBlock("Basic", new VBox(SampleBlock.BLOCK_VGAP, leftToggle, rightToggle));
}
private SampleBlock stateSample() {
var successToggle = new ToggleSwitch("Enabled");
successToggle.selectedProperty().addListener((obs, old, val) -> {
successToggle.setText(val ? "Enabled" : "Disabled");
successToggle.pseudoClassStateChanged(Styles.STATE_SUCCESS, val);
}
);
successToggle.setSelected(true);
successToggle.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
var dangerToggle = new ToggleSwitch("Disabled");
dangerToggle.selectedProperty().addListener((obs, old, val) -> {
dangerToggle.setText(val ? "Enabled" : "Disabled");
dangerToggle.pseudoClassStateChanged(Styles.STATE_DANGER, val);
}
);
dangerToggle.setLabelPosition(HorizontalDirection.RIGHT);
dangerToggle.setSelected(false);
return new SampleBlock("State", new VBox(SampleBlock.BLOCK_VGAP, successToggle, dangerToggle));
}
}

@ -2,46 +2,44 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.ACCENT; import atlantafx.base.controls.CaptionMenuItem;
import static atlantafx.base.theme.Styles.BOTTOM; import atlantafx.base.controls.CustomTextField;
import static atlantafx.base.theme.Styles.BUTTON_ICON;
import static atlantafx.base.theme.Styles.CENTER_PILL;
import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.base.theme.Styles.LEFT;
import static atlantafx.base.theme.Styles.LEFT_PILL;
import static atlantafx.base.theme.Styles.RIGHT;
import static atlantafx.base.theme.Styles.RIGHT_PILL;
import static atlantafx.base.theme.Styles.TOP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static atlantafx.sampler.util.Controls.button;
import static atlantafx.sampler.util.Controls.iconButton;
import static atlantafx.sampler.util.Controls.toggleButton;
import static javafx.geometry.Orientation.HORIZONTAL;
import static javafx.geometry.Orientation.VERTICAL;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch; import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.fake.SampleMenuBar; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Orientation; import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.geometry.Side; import javafx.geometry.Side;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar; import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuButton; import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.Separator; import javafx.scene.control.Separator;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TitledPane; import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
@ -50,10 +48,14 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Font; import javafx.scene.text.Font;
import net.datafaker.Faker;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
public class ToolBarPage extends AbstractPage { public class ToolBarPage extends OutlinePage {
public static final String NAME = "ToolBar"; public static final String NAME = "ToolBar";
@ -62,60 +64,152 @@ public class ToolBarPage extends AbstractPage {
return NAME; return NAME;
} }
private Side toolbarPos = Side.TOP;
public ToolBarPage() { public ToolBarPage() {
super(); super();
createView();
addFormattedText("""
A [i]ToolBar[/i] is a control which displays items horizontally or vertically."""
);
addSection("Usage", usageExample());
addSection("Playground", playground());
} }
private void createView() { private ExampleBox usageExample() {
var toolbar = new ToolBar(createButtons(HORIZONTAL)); //snippet_1:start
var toolbar1 = new ToolBar(
new Button("New", new FontIcon(Feather.PLUS)),
new Button("Open", new FontIcon(Feather.FILE)),
new Button("Save", new FontIcon(Feather.SAVE)),
new Separator(Orientation.VERTICAL),
new Button("Clean", new FontIcon(Feather.ROTATE_CCW)),
new Button("Compile", new FontIcon(Feather.LAYERS)),
new Button("Run", new FontIcon(Feather.PLAY))
);
// ~
var fontFamilyCmb = new ComboBox<>(
FXCollections.observableArrayList(Font.getFamilies())
);
fontFamilyCmb.setPrefWidth(150);
fontFamilyCmb.getSelectionModel().selectFirst();
var fontSizeCmb = new ComboBox<>(
IntStream.range(6, 32).mapToObj(String::valueOf).collect(
Collectors.toCollection(FXCollections::observableArrayList)
));
fontSizeCmb.getSelectionModel().select(6);
var toolbar2 = new ToolBar(
fontFamilyCmb,
fontSizeCmb,
new Separator(Orientation.VERTICAL),
toggleIconButton(Feather.BOLD, true),
toggleIconButton(Feather.ITALIC),
toggleIconButton(Feather.UNDERLINE),
new Separator(Orientation.VERTICAL),
toggleIconButton(Feather.ALIGN_LEFT),
toggleIconButton(Feather.ALIGN_CENTER),
toggleIconButton(Feather.ALIGN_RIGHT),
new Separator(Orientation.VERTICAL),
iconButton(Feather.IMAGE)
);
// ~
var textField = new CustomTextField("https://example.com");
textField.setPromptText("Search Doodle of type an URL");
textField.setLeft(new FontIcon(Feather.LOCK));
textField.setRight(new FontIcon(Feather.STAR));
HBox.setHgrow(textField, Priority.ALWAYS);
var dropdown = new MenuButton("", new FontIcon(Feather.MENU));
dropdown.getItems().setAll(
new MenuItem("Action 1"),
new MenuItem("Action 2"),
new MenuItem("Action 3")
);
var toolbar3 = new ToolBar(
iconButton(Feather.CHEVRON_LEFT),
iconButton(Feather.CHEVRON_RIGHT),
new Separator(Orientation.VERTICAL),
iconButton(Feather.REFRESH_CW),
new Spacer(10),
textField,
new Spacer(10),
iconButton(Feather.BOOKMARK),
iconButton(Feather.USER),
dropdown
);
//snippet_1:end
var box = new VBox(VGAP_20, toolbar1, toolbar2, toolbar3);
var description = BBCodeParser.createFormattedText("""
The most common items to place within a toolbar are [i]Button[/i], [i]ToggleButtons[/i] \
and [i]Separator[/i], but you are not restricted to just these, and can insert any node \
into it."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private Side toolbarPos = Side.TOP;
private VBox playground() {
var toolbar = new ToolBar(createButtons(Orientation.HORIZONTAL));
var toolbarLayer = new BorderPane(); var toolbarLayer = new BorderPane();
toolbarLayer.setTop(new TopBar(toolbar)); toolbarLayer.setTop(new TopBar(toolbar));
var controller = createController(toolbarLayer, toolbar); var controller = createController(toolbarLayer, toolbar);
controller.setPrefSize(500, 300); controller.setPrefSize(500, 300);
var controllerLayer = new BorderPane();
controllerLayer.setCenter(controller); var controllerLayer = new BorderPane(controller);
controllerLayer.setMaxSize(500, 300); controllerLayer.setMaxSize(500, 300);
var root = new StackPane(); var description = BBCodeParser.createFormattedText("""
root.getStyleClass().add(Styles.BORDERED); The playground demonstrates the most important [i]Toolbar[/i] features \
root.getChildren().addAll(toolbarLayer, controllerLayer); and also serves as an object for monkey testing."""
VBox.setVgrow(root, Priority.ALWAYS); );
setUserContent(new SampleBlock("Playground", root)); var stack = new StackPane(toolbarLayer, controllerLayer);
stack.getStyleClass().add(Styles.BORDERED);
stack.setMinSize(600, 500);
return new VBox(VGAP_10, description, stack);
} }
private TitledPane createController(BorderPane borderPane, ToolBar toolbar) { private TitledPane createController(BorderPane borderPane, ToolBar toolbar) {
// == BUTTONS == // == BUTTONS ==
var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP)); var toTopBtn = new Button("", new FontIcon(Feather.ARROW_UP));
toTopBtn.getStyleClass().addAll(BUTTON_ICON); toTopBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toTopBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.TOP)); toTopBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.TOP));
var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT)); var toRightBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT));
toRightBtn.getStyleClass().addAll(BUTTON_ICON); toRightBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toRightBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.RIGHT)); toRightBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.RIGHT));
var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN)); var toBottomBtn = new Button("", new FontIcon(Feather.ARROW_DOWN));
toBottomBtn.getStyleClass().addAll(BUTTON_ICON); toBottomBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toBottomBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.BOTTOM)); toBottomBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.BOTTOM));
var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT)); var toLeftBtn = new Button("", new FontIcon(Feather.ARROW_LEFT));
toLeftBtn.getStyleClass().addAll(BUTTON_ICON); toLeftBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toLeftBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.LEFT)); toLeftBtn.setOnAction(e -> rotateToolbar(borderPane, toolbar, Side.LEFT));
var appendBtn = new Button("", new FontIcon(Feather.PLUS)); var appendBtn = new Button("", new FontIcon(Feather.PLUS));
appendBtn.getStyleClass().addAll(BUTTON_ICON, ACCENT); appendBtn.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.ACCENT);
appendBtn.setOnAction(e -> { appendBtn.setOnAction(e -> {
if (toolbar.getOrientation() == HORIZONTAL) { if (toolbar.getOrientation() == Orientation.HORIZONTAL) {
var textBtn = new Button(FAKER.animal().name(), new FontIcon(randomIcon())); var textBtn = new Button(FAKER.animal().name(), new FontIcon(randomIcon()));
toolbar.getItems().add(textBtn); toolbar.getItems().add(textBtn);
} else { } else {
var iconBtn = new Button("", new FontIcon(randomIcon())); var iconBtn = new Button("", new FontIcon(randomIcon()));
iconBtn.getStyleClass().addAll(BUTTON_ICON); iconBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
toolbar.getItems().add(iconBtn); toolbar.getItems().add(iconBtn);
} }
}); });
@ -124,6 +218,8 @@ public class ToolBarPage extends AbstractPage {
buttonsPane.setMinSize(120, 120); buttonsPane.setMinSize(120, 120);
buttonsPane.setMaxSize(120, 120); buttonsPane.setMaxSize(120, 120);
buttonsPane.setCenter(appendBtn);
buttonsPane.setTop(toTopBtn); buttonsPane.setTop(toTopBtn);
BorderPane.setAlignment(toTopBtn, Pos.CENTER); BorderPane.setAlignment(toTopBtn, Pos.CENTER);
@ -136,14 +232,12 @@ public class ToolBarPage extends AbstractPage {
buttonsPane.setLeft(toLeftBtn); buttonsPane.setLeft(toLeftBtn);
BorderPane.setAlignment(toLeftBtn, Pos.CENTER); BorderPane.setAlignment(toLeftBtn, Pos.CENTER);
buttonsPane.setCenter(appendBtn);
// == TOGGLES == // == TOGGLES ==
var menuBarToggle = new ToggleSwitch(); var menuBarToggle = new ToggleSwitch();
menuBarToggle.selectedProperty().addListener((obs, old, value) -> { menuBarToggle.selectedProperty().addListener((obs, old, val) -> {
TopBar topBar = (TopBar) borderPane.getTop(); TopBar topBar = (TopBar) borderPane.getTop();
if (value) { if (val) {
topBar.showOrCreateMenuBar(); topBar.showOrCreateMenuBar();
} else { } else {
topBar.hideMenuBar(); topBar.hideMenuBar();
@ -160,26 +254,22 @@ public class ToolBarPage extends AbstractPage {
var togglesGrid = new GridPane(); var togglesGrid = new GridPane();
togglesGrid.setHgap(10); togglesGrid.setHgap(10);
togglesGrid.setVgap(10); togglesGrid.setVgap(10);
togglesGrid.addRow(0, createLabel("Show menu bar"), menuBarToggle);
togglesGrid.add(createLabel("Show menu bar"), 0, 0); togglesGrid.addRow(1, createLabel("Disable"), disableToggle);
togglesGrid.add(menuBarToggle, 1, 0);
togglesGrid.add(createLabel("Disable"), 0, 1);
togglesGrid.add(disableToggle, 1, 1);
// == LAYOUT == // == LAYOUT ==
var controls = new HBox(40, new Spacer(), buttonsPane, togglesGrid, new Spacer()); var controls = new HBox(40, buttonsPane, togglesGrid);
controls.setAlignment(Pos.CENTER); controls.setAlignment(Pos.CENTER);
controls.setFillHeight(true);
var content = new VBox(BLOCK_VGAP); var wrapper = new VBox(VGAP_20, controls);
content.getChildren().setAll(controls); wrapper.setAlignment(Pos.CENTER);
content.setAlignment(Pos.CENTER);
var root = new TitledPane("Controller", content); var controller = new TitledPane("Controller", wrapper);
root.setCollapsible(false); controller.setCollapsible(false);
return root; return controller;
} }
private void rotateToolbar(BorderPane borderPane, ToolBar toolbar, Side pos) { private void rotateToolbar(BorderPane borderPane, ToolBar toolbar, Side pos) {
@ -204,27 +294,35 @@ public class ToolBarPage extends AbstractPage {
Platform.runLater(() -> { Platform.runLater(() -> {
switch (pos) { switch (pos) {
case TOP -> { case TOP -> {
toolbar.setOrientation(HORIZONTAL); toolbar.setOrientation(Orientation.HORIZONTAL);
Styles.addStyleClass(toolbar, TOP, RIGHT, BOTTOM, LEFT); Styles.addStyleClass(
toolbar.getItems().setAll(createButtons(HORIZONTAL)); toolbar, Styles.TOP, Styles.RIGHT, Styles.BOTTOM, Styles.LEFT
);
toolbar.getItems().setAll(createButtons(Orientation.HORIZONTAL));
topBar.setToolBar(toolbar); topBar.setToolBar(toolbar);
} }
case RIGHT -> { case RIGHT -> {
toolbar.setOrientation(VERTICAL); toolbar.setOrientation(Orientation.VERTICAL);
Styles.addStyleClass(toolbar, RIGHT, TOP, BOTTOM, LEFT); Styles.addStyleClass(
toolbar.getItems().setAll(createButtons(VERTICAL)); toolbar, Styles.RIGHT, Styles.TOP, Styles.BOTTOM, Styles.LEFT
);
toolbar.getItems().setAll(createButtons(Orientation.VERTICAL));
borderPane.setRight(toolbar); borderPane.setRight(toolbar);
} }
case BOTTOM -> { case BOTTOM -> {
toolbar.setOrientation(HORIZONTAL); toolbar.setOrientation(Orientation.HORIZONTAL);
Styles.addStyleClass(toolbar, BOTTOM, TOP, RIGHT, LEFT); Styles.addStyleClass(
toolbar.getItems().setAll(createButtons(HORIZONTAL)); toolbar, Styles.BOTTOM, Styles.TOP, Styles.RIGHT, Styles.LEFT
);
toolbar.getItems().setAll(createButtons(Orientation.HORIZONTAL));
borderPane.setBottom(toolbar); borderPane.setBottom(toolbar);
} }
case LEFT -> { case LEFT -> {
toolbar.setOrientation(VERTICAL); toolbar.setOrientation(Orientation.VERTICAL);
Styles.addStyleClass(toolbar, LEFT, RIGHT, TOP, BOTTOM); Styles.addStyleClass(
toolbar.getItems().setAll(createButtons(VERTICAL)); toolbar, Styles.LEFT, Styles.RIGHT, Styles.TOP, Styles.BOTTOM
);
toolbar.getItems().setAll(createButtons(Orientation.VERTICAL));
borderPane.setLeft(toolbar); borderPane.setLeft(toolbar);
} }
} }
@ -233,39 +331,25 @@ public class ToolBarPage extends AbstractPage {
public Node[] createButtons(Orientation orientation) { public Node[] createButtons(Orientation orientation) {
var result = new ArrayList<Node>(); var result = new ArrayList<Node>();
result.add(iconButton(Feather.FILE, false)); result.add(iconButton(Feather.FILE));
result.add(iconButton(Feather.FOLDER, false)); result.add(iconButton(Feather.FOLDER));
result.add(iconButton(Feather.SAVE, false)); result.add(iconButton(Feather.SAVE));
result.add(new Separator()); result.add(new Separator());
if (orientation == HORIZONTAL) { if (orientation == Orientation.HORIZONTAL) {
result.add(button("Undo", null, false)); result.add(new Button("Undo"));
result.add(button("Redo", null, true)); result.add(new Button("Redo"));
result.add(new Separator()); result.add(new Separator());
result.add(toggleIconButton(Feather.BOLD, true, Styles.LEFT_PILL));
result.add(toggleButton("", Feather.BOLD, null, true, BUTTON_ICON, LEFT_PILL)); result.add(toggleIconButton(Feather.ITALIC, Styles.CENTER_PILL));
result.add(toggleButton("", Feather.ITALIC, null, false, BUTTON_ICON, CENTER_PILL)); result.add(toggleIconButton(Feather.UNDERLINE, Styles.RIGHT_PILL));
result.add(toggleButton("", Feather.UNDERLINE, null, false, BUTTON_ICON, RIGHT_PILL));
result.add(new Spacer(5));
var fontCombo = new ComboBox<>(FXCollections.observableArrayList(Font.getFamilies()));
fontCombo.setPrefWidth(150);
fontCombo.getSelectionModel().selectFirst();
result.add(fontCombo);
var settingsMenu = new MenuButton("Settings", new FontIcon(Feather.SETTINGS), createItems(5));
settingsMenu.getStyleClass().add(FLAT);
result.add(new Spacer());
result.add(settingsMenu);
} }
if (orientation == VERTICAL) { if (orientation == Orientation.VERTICAL) {
result.add(iconButton(Feather.CORNER_DOWN_LEFT, false)); result.add(iconButton(Feather.CORNER_DOWN_LEFT));
result.add(iconButton(Feather.CORNER_DOWN_RIGHT, true)); result.add(iconButton(Feather.CORNER_DOWN_RIGHT));
result.add(new Spacer(orientation)); result.add(new Spacer(orientation));
result.add(iconButton(Feather.SETTINGS, false)); result.add(iconButton(Feather.SETTINGS));
} }
return result.toArray(Node[]::new); return result.toArray(Node[]::new);
@ -278,9 +362,42 @@ public class ToolBarPage extends AbstractPage {
return label; return label;
} }
public static MenuItem[] createItems(int count) { public static Button iconButton(Ikon icon) {
return IntStream.range(0, count).mapToObj(i -> new MenuItem(FAKER.babylon5().character())) var btn = new Button("");
.toArray(MenuItem[]::new); if (icon != null) {
btn.setGraphic(new FontIcon(icon));
}
btn.getStyleClass().addAll(Styles.BUTTON_ICON);
return btn;
}
public ToggleButton toggleIconButton(@Nullable Ikon icon,
String... styleClasses) {
return toggleIconButton(icon, null, false, styleClasses);
}
public ToggleButton toggleIconButton(@Nullable Ikon icon,
boolean selected,
String... styleClasses) {
return toggleIconButton(icon, null, selected, styleClasses);
}
public ToggleButton toggleIconButton(@Nullable Ikon icon,
@Nullable ToggleGroup group,
boolean selected,
String... styleClasses) {
var btn = new ToggleButton("");
if (icon != null) {
btn.setGraphic(new FontIcon(icon));
}
if (group != null) {
btn.setToggleGroup(group);
}
btn.getStyleClass().addAll(styleClasses);
btn.setSelected(selected);
return btn;
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -318,4 +435,188 @@ public class ToolBarPage extends AbstractPage {
getChildren().set(1, DUMMY_TOOLBAR); getChildren().set(1, DUMMY_TOOLBAR);
} }
} }
public static class SampleMenuBar extends MenuBar {
private static final EventHandler<ActionEvent> PRINT_SOURCE = System.out::println;
public SampleMenuBar(Faker faker) {
getMenus().addAll(
fileMenu(faker),
editMenu(),
viewMenu(),
toolsMenu(),
aboutMenu()
);
}
private Menu fileMenu(Faker faker) {
var fileMenu = new Menu("_File");
fileMenu.setMnemonicParsing(true);
fileMenu.setOnAction(PRINT_SOURCE);
var newMenu = menuItem(
"_New", null,
new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)
);
newMenu.setMnemonicParsing(true);
newMenu.setOnAction(PRINT_SOURCE);
var openRecentMenu = new Menu("Open _Recent");
openRecentMenu.setMnemonicParsing(true);
openRecentMenu.setOnAction(PRINT_SOURCE);
openRecentMenu.getItems().addAll(
IntStream.range(0, 10)
.mapToObj(x -> new MenuItem(faker.file().fileName()))
.toList()
);
fileMenu.getItems().addAll(
newMenu,
new SeparatorMenuItem(),
menuItem(
"Open", Feather.FOLDER,
new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN)
),
openRecentMenu,
new SeparatorMenuItem(),
menuItem(
"Save", Feather.SAVE,
new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN)
),
new MenuItem("Save As"),
new SeparatorMenuItem(),
new MenuItem("Exit")
);
return fileMenu;
}
private Menu editMenu() {
var editMenu = new Menu("_Edit");
editMenu.setMnemonicParsing(true);
editMenu.setOnAction(PRINT_SOURCE);
var copyItem = menuItem(
"Copy", Feather.COPY,
new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_DOWN)
);
copyItem.setDisable(true);
editMenu.getItems().addAll(
menuItem(
"Undo", Feather.CORNER_DOWN_LEFT,
new KeyCodeCombination(KeyCode.Z, KeyCombination.CONTROL_DOWN)
),
menuItem("Redo", Feather.CORNER_DOWN_RIGHT,
new KeyCodeCombination(KeyCode.Y, KeyCombination.CONTROL_DOWN)
),
new SeparatorMenuItem(),
menuItem(
"Cut", Feather.SCISSORS,
new KeyCodeCombination(KeyCode.X, KeyCombination.CONTROL_DOWN)
),
copyItem,
menuItem(
"Paste", Feather.CORNER_DOWN_LEFT,
new KeyCodeCombination(KeyCode.V, KeyCombination.CONTROL_DOWN)
)
);
return editMenu;
}
private Menu viewMenu() {
var viewMenu = new Menu("_View");
viewMenu.setMnemonicParsing(true);
viewMenu.setOnAction(PRINT_SOURCE);
var showToolbarItem = new CheckMenuItem(
"Show Toolbar", new FontIcon(Feather.TOOL)
);
showToolbarItem.setSelected(true);
showToolbarItem.setAccelerator(
new KeyCodeCombination(KeyCode.T, KeyCombination.CONTROL_DOWN)
);
var showGridItem = new CheckMenuItem(
"Show Grid", new FontIcon(Feather.GRID)
);
var captionItem = new CaptionMenuItem("Layout");
var viewToggleGroup = new ToggleGroup();
var toggleItem1 = new RadioMenuItem(
"Single", new FontIcon(Material2OutlinedAL.LOOKS_ONE)
);
toggleItem1.setSelected(true);
toggleItem1.setToggleGroup(viewToggleGroup);
var toggleItem2 = new RadioMenuItem(
"Two Columns", new FontIcon(Material2OutlinedAL.LOOKS_TWO)
);
toggleItem2.setToggleGroup(viewToggleGroup);
var toggleItem3 = new RadioMenuItem(
"Three Columns", new FontIcon(Material2OutlinedAL.LOOKS_3)
);
toggleItem3.setToggleGroup(viewToggleGroup);
viewMenu.getItems().addAll(
showToolbarItem,
showGridItem,
new SeparatorMenuItem(),
captionItem,
toggleItem1,
toggleItem2,
toggleItem3
);
return viewMenu;
}
private Menu toolsMenu() {
var toolsMenu = new Menu("_Tools");
toolsMenu.setMnemonicParsing(true);
toolsMenu.setOnAction(PRINT_SOURCE);
toolsMenu.setDisable(true);
return toolsMenu;
}
private Menu aboutMenu() {
var aboutMenu = new Menu("_About", new FontIcon(Feather.HELP_CIRCLE));
aboutMenu.setMnemonicParsing(true);
aboutMenu.setOnAction(PRINT_SOURCE);
var deeplyNestedMenu = new Menu("Very...", null,
new Menu("Very...", null,
new Menu("Deeply", null,
new Menu("Nested", null,
new MenuItem("Menu")
))));
// NOTE: this won't be displayed because right container is reserved for submenu indication
deeplyNestedMenu.setAccelerator(new KeyCodeCombination(
KeyCode.DIGIT1, KeyCombination.SHIFT_DOWN, KeyCombination.CONTROL_DOWN)
);
aboutMenu.getItems().addAll(
new MenuItem("Help"),
new MenuItem("About"),
new SeparatorMenuItem(),
deeplyNestedMenu
);
return aboutMenu;
}
public MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator) {
var item = new MenuItem(text);
if (graphic != null) {
item.setGraphic(new FontIcon(graphic));
}
if (accelerator != null) {
item.setAccelerator(accelerator);
}
return item;
}
}
} }

@ -2,24 +2,19 @@
package atlantafx.sampler.page.components; package atlantafx.sampler.page.components;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP; import atlantafx.base.util.BBCodeParser;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP; import atlantafx.sampler.page.ExampleBox;
import static javafx.geometry.Orientation.VERTICAL; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.SampleBlock;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.PopupWindow.AnchorLocation; import javafx.stage.PopupWindow.AnchorLocation;
import javafx.util.Duration; import javafx.util.Duration;
public class TooltipPage extends AbstractPage { public class TooltipPage extends OutlinePage {
public static final String NAME = "Tooltip"; public static final String NAME = "Tooltip";
@ -30,88 +25,87 @@ public class TooltipPage extends AbstractPage {
public TooltipPage() { public TooltipPage() {
super(); super();
setUserContent(new VBox(Page.PAGE_VGAP,
expandingHBox( addFormattedText("""
basicSample(), Tooltips are used for showing additional information when the node is hovered over by the mouse.
textWrapSample(),
indefiniteSample() [ul]
), [li]Any node can show a tooltip.[/li]
positionSample() [li]A Tooltip is able to show within it an arbitrary scenegraph of nodes.[/li]
)); [li]A single tooltip can be installed on multiple target nodes or multiple controls.[/li][/ul]""");
addSection("Usage", usageExample());
addSection("Position", positionExample());
} }
private SampleBlock basicSample() { private ExampleBox usageExample() {
var tooltip = new Tooltip(FAKER.harryPotter().spell()); //snippet_1:start
tooltip.setHideDelay(Duration.seconds(3)); var basicTtp = new Tooltip(FAKER.harryPotter().spell());
basicTtp.setHideDelay(Duration.seconds(3));
var label = createLabel("Hover me"); var basicLbl = createLabel("Basic");
label.setTooltip(tooltip); basicLbl.setTooltip(basicTtp);
return new SampleBlock("Basic", label); var longTtp = new Tooltip(FAKER.lorem().paragraph(5));
} longTtp.setHideDelay(Duration.seconds(3));
longTtp.setPrefWidth(200);
longTtp.setWrapText(true);
private SampleBlock textWrapSample() { var longLbl = createLabel("Long Text");
var tooltip = new Tooltip(FAKER.lorem().paragraph(5)); longLbl.setTooltip(longTtp);
tooltip.setHideDelay(Duration.seconds(3)); //snippet_1:end
tooltip.setPrefWidth(200);
tooltip.setWrapText(true);
var label = createLabel("Hover me"); var box = new HBox(HGAP_20, basicLbl, longLbl);
label.setTooltip(tooltip);
return new SampleBlock("Text Wrap", label); var description = BBCodeParser.createFormattedText("""
} In most cases a [i]Tooltip[/i] is created and its text property is \
modified to show plain text to the user."""
private SampleBlock indefiniteSample() {
var tooltip = new Tooltip(FAKER.harryPotter().spell());
tooltip.setHideDelay(Duration.INDEFINITE);
var label = createLabel("Hover me");
label.setTooltip(tooltip);
return new SampleBlock("Indefinite", label);
}
private SampleBlock positionSample() {
var topLeftLabel = createLabel("Top Left");
topLeftLabel.setTooltip(createTooltip("Top Left", AnchorLocation.WINDOW_BOTTOM_RIGHT));
var topRightLabel = createLabel("Top Right");
topRightLabel.setTooltip(createTooltip("Top Right", AnchorLocation.WINDOW_BOTTOM_LEFT));
var bottomLeftLabel = createLabel("Bottom Left");
bottomLeftLabel.setTooltip(createTooltip("Bottom Left", AnchorLocation.WINDOW_TOP_RIGHT));
var bottomRightLabel = createLabel("Bottom Right");
bottomRightLabel.setTooltip(createTooltip("Bottom Right", AnchorLocation.WINDOW_TOP_LEFT));
var flowPane = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP,
topLeftLabel,
new Separator(VERTICAL),
topRightLabel,
new Separator(VERTICAL),
bottomLeftLabel,
new Separator(VERTICAL),
bottomRightLabel
); );
return new SampleBlock("Position", flowPane); return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox positionExample() {
//snippet_2:start
// if the tooltip disappears almost immediately,
// it's a #javafx-bug (you'll never walk alone)
var topLeftTtp = new Tooltip(FAKER.chuckNorris().fact());
topLeftTtp.setAnchorLocation(AnchorLocation.WINDOW_BOTTOM_RIGHT);
var topLeftLbl = createLabel("Top Left");
topLeftLbl.setTooltip(topLeftTtp);
var topRightTtp = new Tooltip(FAKER.chuckNorris().fact());
topRightTtp.setAnchorLocation(AnchorLocation.WINDOW_BOTTOM_LEFT);
var topRightLbl = createLabel("Top Right");
topRightLbl.setTooltip(topRightTtp);
var bottomLeftTtp = new Tooltip(FAKER.chuckNorris().fact());
bottomLeftTtp.setAnchorLocation(AnchorLocation.WINDOW_TOP_RIGHT);
var bottomLeftLbl = createLabel("Bottom Left");
bottomLeftLbl.setTooltip(bottomLeftTtp);
var bottomRightTtp = new Tooltip(FAKER.chuckNorris().fact());
bottomRightTtp.setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT);
var bottomRightLbl = createLabel("Bottom Right");
bottomRightLbl.setTooltip(bottomRightTtp);
//snippet_2:end
var box = new HBox(HGAP_20, topLeftLbl, topRightLbl, bottomLeftLbl, bottomRightLbl);
var description = BBCodeParser.createFormattedText("""
You can specify the popup anchor point which is used in [i]Tooltip[/i] positioning. \
The point can be set to a corner of the popup window or a corner of its content."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
} }
private Label createLabel(String text) { private Label createLabel(String text) {
Label label = new Label(text); Label label = new Label(text);
label.setMinWidth(50); label.setMinWidth(100);
label.setMinHeight(50); label.setMinHeight(50);
label.setPadding(new Insets(10)); label.setPadding(new Insets(10));
label.setStyle("-fx-background-color:-color-accent-subtle;"); label.setStyle("-fx-background-color:-color-accent-subtle;");
label.setAlignment(Pos.CENTER_LEFT); label.setAlignment(Pos.CENTER);
return label; return label;
} }
private Tooltip createTooltip(String text, AnchorLocation arrowPos) {
var tooltip = new Tooltip(text);
tooltip.setAnchorLocation(arrowPos);
return tooltip;
}
} }

@ -1,318 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.control.cell.ChoiceBoxTreeCell;
import javafx.scene.control.cell.ComboBoxTreeCell;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class TreePage extends AbstractPage {
public static final String NAME = "TreeView";
private static final int MAX_TREE_DEPTH = 3;
private static final int[] TREE_DICE = {-1, 0, 1};
@Override
public String getName() {
return NAME;
}
private final BorderPane treeWrapper = new BorderPane();
private final ComboBox<Example> exampleSelect = createExampleSelect();
public TreePage() {
super();
var sample = new SampleBlock("Playground", createPlayground());
sample.setFillHeight(true);
setUserContent(sample);
}
private VBox createPlayground() {
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, value) -> findDisplayedTree().ifPresent(tv -> toggleStyleClass(tv, DENSE))
);
var showRootToggle = new ToggleSwitch("Show root");
showRootToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedTree().ifPresent(tv -> {
if (val != null) {
tv.setShowRoot(val);
}
}));
showRootToggle.setSelected(true);
var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener((obs, old, val) ->
findDisplayedTree().ifPresent(tv -> toggleStyleClass(tv, Tweaks.ALT_ICON))
);
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, val) -> findDisplayedTree().ifPresent(tv -> toggleStyleClass(tv, Tweaks.EDGE_TO_EDGE))
);
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> findDisplayedTree().ifPresent(tv -> {
if (val != null) {
tv.setDisable(val);
}
}));
var controls = new HBox(BLOCK_HGAP, denseToggle, showRootToggle, altIconToggle, edge2edgeToggle);
controls.setAlignment(Pos.CENTER);
VBox.setVgrow(treeWrapper, Priority.ALWAYS);
var playground = new VBox(
BLOCK_VGAP,
new HBox(new Label("Select an example:"), new Spacer(), disableToggle),
exampleSelect,
treeWrapper,
controls
);
playground.setMinHeight(100);
return playground;
}
private ComboBox<Example> createExampleSelect() {
var select = new ComboBox<Example>();
select.setMaxWidth(Double.MAX_VALUE);
select.getItems().setAll(Example.values());
select.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
TreeView<String> newTree = createTree(val);
// copy existing style classes and properties to the new tree
findDisplayedTree().ifPresent(tv -> {
List<String> currentStyles = tv.getStyleClass();
currentStyles.remove("tree-view");
newTree.getStyleClass().addAll(currentStyles);
newTree.setShowRoot(tv.isShowRoot());
newTree.setDisable(tv.isDisable());
});
treeWrapper.setCenter(newTree);
});
select.setConverter(new StringConverter<>() {
@Override
public String toString(Example example) {
return example == null ? "" : example.getName();
}
@Override
public Example fromString(String s) {
return Example.find(s);
}
});
return select;
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private Optional<TreeView<?>> findDisplayedTree() {
return treeWrapper.getChildren().size() > 0
? Optional.of((TreeView<?>) treeWrapper.getChildren().get(0))
: Optional.empty();
}
private TreeView<String> createTree(Example example) {
switch (example) {
case TEXT -> {
return stringTree();
}
case GRAPHIC -> {
return graphicTree();
}
case EDITABLE -> {
return editableTree();
}
case CHECK_BOX -> {
return checkBoxTree();
}
case CHOICE_BOX -> {
return choiceBoxTree();
}
case COMBO_BOX -> {
return comboBoxTree();
}
default -> throw new IllegalArgumentException("Unexpected enum value: " + example);
}
}
private <T> void generateTree(TreeItem<T> parent, Supplier<TreeItem<T>> supplier, int limit, int depth) {
if (limit == 0) {
return;
}
var item = supplier.get();
parent.getChildren().add(item);
TreeItem<T> nextParent = parent; // sibling
int nextDepth = depth;
int rand = TREE_DICE[RANDOM.nextInt(TREE_DICE.length)];
if (rand < 0 && parent.getParent() != null) { // go up
nextParent = parent.getParent();
nextDepth = --depth;
}
if (rand > 0 && depth < MAX_TREE_DEPTH) { // go down
nextParent = item;
nextDepth = ++depth;
}
generateTree(nextParent, supplier, --limit, nextDepth);
}
private TreeView<String> stringTree() {
var root = new TreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord()), 30, 1);
tree.setRoot(root);
return tree;
}
private TreeView<String> graphicTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1);
tree.setRoot(root);
return tree;
}
private TreeView<String> editableTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(TextFieldTreeCell.forTreeView());
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1);
tree.setRoot(root);
return tree;
}
// Note that CheckBoxTreeCell is incompatible with user graphic,
// because it adds graphic inside .checkbox container. #javafx-bug
private TreeView<String> checkBoxTree() {
var root = new CheckBoxTreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(CheckBoxTreeCell.forTreeView());
generateTree(root, () -> new CheckBoxTreeItem<>(FAKER.internet().domainWord()), 30, 1);
tree.setRoot(root);
return tree;
}
private TreeView<String> choiceBoxTree() {
var root = new TreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(ChoiceBoxTreeCell.forTreeView(
generate(() -> FAKER.internet().domainWord(), 10).toArray(String[]::new)
));
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord()), 30, 1);
tree.setRoot(root);
return tree;
}
private TreeView<String> comboBoxTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(ComboBoxTreeCell.forTreeView(
generate(() -> FAKER.internet().domainWord(), 10).toArray(String[]::new)
));
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1);
tree.setRoot(root);
return tree;
}
///////////////////////////////////////////////////////////////////////////
private enum Example {
TEXT("Text"),
GRAPHIC("Text with icons"),
EDITABLE("TextFieldTreeCell"),
CHECK_BOX("CheckBoxTreeCell"),
CHOICE_BOX("ChoiceBoxTreeCell"),
COMBO_BOX("ComboBoxTreeCell");
private final String name;
Example(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Example find(String name) {
return Arrays.stream(Example.values())
.filter(example -> Objects.equals(example.getName(), name))
.findFirst()
.orElse(null);
}
}
}

@ -1,315 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BORDERED;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.STRIPED;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.base.theme.Tweaks.ALIGN_CENTER;
import static atlantafx.base.theme.Tweaks.ALIGN_LEFT;
import static atlantafx.base.theme.Tweaks.ALIGN_RIGHT;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.MenuButton;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.scene.control.cell.ChoiceBoxTreeTableCell;
import javafx.scene.control.cell.ComboBoxTreeTableCell;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class TreeTablePage extends AbstractPage {
public static final String NAME = "TreeTableView";
@Override
public String getName() {
return NAME;
}
private TreeTableView<Product> treeTable;
public TreeTablePage() {
super();
var sample = new SampleBlock("Playground", createPlayground());
sample.setFillHeight(true);
setUserContent(sample);
}
private VBox createPlayground() {
// == FOOTER ==
var bordersToggle = new ToggleSwitch("Bordered");
bordersToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(treeTable, BORDERED));
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(treeTable, DENSE));
var stripesToggle = new ToggleSwitch("Striped");
stripesToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(treeTable, STRIPED));
var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener((obs, old, val) -> toggleStyleClass(treeTable, Tweaks.ALT_ICON));
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, value) -> toggleStyleClass(treeTable, Tweaks.EDGE_TO_EDGE)
);
var footer = new HBox(BLOCK_HGAP, bordersToggle, denseToggle, stripesToggle, altIconToggle, edge2edgeToggle);
footer.setAlignment(Pos.CENTER);
// == TREE TABLE ==
var rootVal = Product.empty(0);
rootVal.setBrand("Root");
var root = new TreeItem<>(rootVal);
for (int idx = 1; idx <= FAKER.random().nextInt(5, 10); idx++) {
String brand = FAKER.commerce().brand();
var groupVal = Product.empty(0);
groupVal.setBrand(brand);
var group = new TreeItem<>(groupVal);
group.getChildren().setAll(
createTreeItems(idx * 100, FAKER.random().nextInt(5, 10), brand)
);
root.getChildren().add(group);
}
treeTable = createTreeTable();
treeTable.setRoot(root);
VBox.setVgrow(treeTable, Priority.ALWAYS);
// == HEADER ==
var alignGroup = new ToggleGroup();
var alignLeftBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_LEFT));
alignLeftBtn.getStyleClass().add(Styles.LEFT_PILL);
alignLeftBtn.setToggleGroup(alignGroup);
alignLeftBtn.setSelected(true);
alignLeftBtn.setOnAction(e -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
c.getStyleClass().removeAll(ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT);
}
});
var alignCenterBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_CENTER));
alignCenterBtn.getStyleClass().add(Styles.CENTER_PILL);
alignCenterBtn.setToggleGroup(alignGroup);
alignCenterBtn.selectedProperty().addListener((obs, old, val) -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
addStyleClass(c, ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT);
}
});
var alignRightBtn = new ToggleButton("", new FontIcon(Feather.ALIGN_RIGHT));
alignRightBtn.getStyleClass().add(Styles.RIGHT_PILL);
alignRightBtn.setToggleGroup(alignGroup);
alignRightBtn.selectedProperty().addListener((obs, old, val) -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
addStyleClass(c, ALIGN_RIGHT, ALIGN_LEFT, ALIGN_CENTER);
}
});
var alignBox = new HBox(alignLeftBtn, alignCenterBtn, alignRightBtn);
var disableToggle = new ToggleSwitch("Disable");
disableToggle.selectedProperty().addListener((obs, old, val) -> {
if (val != null) {
treeTable.setDisable(val);
}
});
var header = new HBox(
createPropertiesMenu(treeTable),
new Spacer(),
alignBox,
new Spacer(),
disableToggle
);
header.setAlignment(Pos.CENTER_LEFT);
// ~
var playground = new VBox(BLOCK_VGAP, header, treeTable, footer);
playground.setMinHeight(100);
return playground;
}
private List<TreeItem<Product>> createTreeItems(int startId, int count, String brand) {
return IntStream.range(startId, startId + count + 1).boxed()
.map(id -> Product.random(id, brand, FAKER))
.map(TreeItem::new)
.toList();
}
@SuppressWarnings("unchecked")
private TreeTableView<Product> createTreeTable() {
var arrowCol = new TreeTableColumn<Product, String>("#");
// This is placeholder column for disclosure nodes. We need to fill it
// with empty strings or all .tree-table-cell will be marked as :empty,
// which in turn leads to absent borders.
arrowCol.setCellValueFactory(cell -> new SimpleStringProperty(""));
arrowCol.setMinWidth(50);
arrowCol.setMaxWidth(50);
var stateCol = new TreeTableColumn<Product, Boolean>("Selected");
stateCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("state"));
stateCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(stateCol));
stateCol.setEditable(true);
var idCol = new TreeTableColumn<Product, String>("ID");
idCol.setCellValueFactory(cell -> {
Product product = cell.getValue().getValue();
return new SimpleStringProperty(
product != null && product.getId() != 0 ? String.valueOf(product.getId()) : ""
);
});
idCol.setEditable(false);
idCol.setMinWidth(80);
var brandCol = new TreeTableColumn<Product, String>("Brand 🖉");
brandCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("brand"));
brandCol.setCellFactory(ChoiceBoxTreeTableCell.forTreeTableColumn(
generate(() -> FAKER.commerce().brand(), 10).toArray(String[]::new)
));
brandCol.setEditable(true);
var nameCol = new TreeTableColumn<Product, String>("Name 🖉");
nameCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
nameCol.setCellFactory(ComboBoxTreeTableCell.forTreeTableColumn(
generate(() -> FAKER.commerce().productName(), 10).toArray(String[]::new)
));
nameCol.setEditable(true);
nameCol.setMinWidth(200);
var priceCol = new TreeTableColumn<Product, String>("Price 🖉");
priceCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("price"));
priceCol.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
priceCol.setEditable(true);
var table = new TreeTableView<Product>();
table.getColumns().setAll(arrowCol, stateCol, brandCol, idCol, nameCol, priceCol);
return table;
}
private MenuButton createPropertiesMenu(TreeTableView<Product> treeTable) {
final var resizePolicyCaption = new CaptionMenuItem("Resize Policy");
final var resizePolicyGroup = new ToggleGroup();
resizePolicyGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof Callback<?, ?> policy) {
//noinspection rawtypes,unchecked
treeTable.setColumnResizePolicy((Callback<TreeTableView.ResizeFeatures, Boolean>) policy);
}
});
final var unconstrainedResizeItem = new RadioMenuItem("Unconstrained");
unconstrainedResizeItem.setToggleGroup(resizePolicyGroup);
unconstrainedResizeItem.setUserData(TreeTableView.UNCONSTRAINED_RESIZE_POLICY);
unconstrainedResizeItem.setSelected(true);
final var constrainedResizeItem = new RadioMenuItem("Constrained");
constrainedResizeItem.setToggleGroup(resizePolicyGroup);
constrainedResizeItem.setUserData(TreeTableView.CONSTRAINED_RESIZE_POLICY);
// ~
final var selectionModeCaption = new CaptionMenuItem("Selection Mode");
final var selectionModeGroup = new ToggleGroup();
selectionModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
treeTable.getSelectionModel().setSelectionMode(mode);
}
});
final var singleSelectionItem = new RadioMenuItem("Single");
singleSelectionItem.setToggleGroup(selectionModeGroup);
singleSelectionItem.setUserData(SelectionMode.SINGLE);
final var multiSelectionItem = new RadioMenuItem("Multiple");
multiSelectionItem.setToggleGroup(selectionModeGroup);
multiSelectionItem.setUserData(SelectionMode.MULTIPLE);
multiSelectionItem.setSelected(true);
// ~
final var showRootItem = new CheckMenuItem("Show root");
treeTable.showRootProperty().bind(showRootItem.selectedProperty());
showRootItem.setSelected(true);
final var editCellsItem = new CheckMenuItem("Editable");
treeTable.editableProperty().bind(editCellsItem.selectedProperty());
editCellsItem.setSelected(true);
final var cellSelectionItem = new CheckMenuItem("Enable cell selection");
treeTable.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelectionItem.selectedProperty());
cellSelectionItem.setSelected(false);
// ~
final var menuButtonItem = new CheckMenuItem("Show menu button");
treeTable.tableMenuButtonVisibleProperty().bind(menuButtonItem.selectedProperty());
menuButtonItem.setSelected(true);
final var propertiesMenu = new MenuButton("Properties");
propertiesMenu.getItems().setAll(
resizePolicyCaption,
unconstrainedResizeItem,
constrainedResizeItem,
selectionModeCaption,
singleSelectionItem,
multiSelectionItem,
new SeparatorMenuItem(),
showRootItem,
editCellsItem,
cellSelectionItem,
menuButtonItem
);
return propertiesMenu;
}
private static void addStyleClass(TreeTableColumn<?, ?> c, String styleClass, String... excludes) {
Objects.requireNonNull(c);
Objects.requireNonNull(styleClass);
if (excludes != null && excludes.length > 0) {
c.getStyleClass().removeAll(excludes);
}
c.getStyleClass().add(styleClass);
}
}

@ -0,0 +1,384 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import static atlantafx.base.theme.Styles.BORDERED;
import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.STRIPED;
import static atlantafx.base.theme.Styles.toggleStyleClass;
import static atlantafx.base.theme.Tweaks.ALIGN_CENTER;
import static atlantafx.base.theme.Tweaks.ALIGN_LEFT;
import static atlantafx.base.theme.Tweaks.ALIGN_RIGHT;
import atlantafx.base.controls.CaptionMenuItem;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.fake.domain.Product;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuButton;
import javafx.scene.control.RadioMenuItem;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.cell.CheckBoxTreeTableCell;
import javafx.scene.control.cell.ChoiceBoxTreeTableCell;
import javafx.scene.control.cell.ComboBoxTreeTableCell;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.control.cell.TreeItemPropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
public class TreeTableViewPage extends OutlinePage {
public static final String NAME = "TreeTableView";
@Override
public String getName() {
return NAME;
}
private TreeTableView<Product> treeTable;
public TreeTableViewPage() {
super();
addFormattedText("""
The [i]TreeTableView[/i] control is conceptually very similar to the \
[i]TreeView[/i] and [i]TableView[/i] controls and basically supports the \
same features. Please, see the corresponding pages for more examples.""");
addSection("Usage", usageExample());
addSection("Playground", playground());
}
@SuppressWarnings("unchecked")
private ExampleBox usageExample() {
//snippet_1:start
record Vehicle(String brand, String model) {
public static Vehicle random(String make) {
return new Vehicle(make, FAKER.vehicle().model());
}
}
var col1 = new TreeTableColumn<Vehicle, String>("Brand");
var col2 = new TreeTableColumn<Vehicle, String>("Model");
col1.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().getValue().brand())
);
col2.setCellValueFactory(
c -> new SimpleStringProperty(c.getValue().getValue().model())
);
var treeTable = new TreeTableView<Vehicle>();
treeTable.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
treeTable.getColumns().setAll(col1, col2);
var make1 = FAKER.vehicle().make();
var treeItem1 = new TreeItem<Vehicle>(new Vehicle(make1, "..."));
treeItem1.getChildren().setAll(
new TreeItem<>(Vehicle.random(make1)),
new TreeItem<>(Vehicle.random(make1)),
new TreeItem<>(Vehicle.random(make1))
);
var make2 = FAKER.vehicle().make();
var treeItem2 = new TreeItem<Vehicle>(new Vehicle(make2, "..."));
treeItem2.getChildren().setAll(
new TreeItem<>(Vehicle.random(make1)),
new TreeItem<>(Vehicle.random(make1)),
new TreeItem<>(Vehicle.random(make1))
);
var rootItem = new TreeItem<Vehicle>(new Vehicle("Vehicles", "..."));
rootItem.getChildren().setAll(treeItem1, treeItem2);
rootItem.setExpanded(true);
treeTable.setRoot(rootItem);
//snippet_1:end
treeTable.setMaxWidth(Double.MAX_VALUE);
treeTable.setMinHeight(300);
HBox.setHgrow(treeTable, Priority.ALWAYS);
var box = new HBox(treeTable);
var description = BBCodeParser.createFormattedText("""
You can create a table view by instantiating the \
[font=monospace]javafx.scene.control.TreeTableView[/font] class."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private VBox playground() {
// == FOOTER ==
var borderToggle = new ToggleSwitch("Bordered");
borderToggle.selectedProperty().addListener(
(obs, old, val) -> toggleStyleClass(treeTable, BORDERED)
);
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener(
(obs, old, val) -> toggleStyleClass(treeTable, DENSE)
);
var stripeToggle = new ToggleSwitch("Striped");
stripeToggle.selectedProperty().addListener(
(obs, old, val) -> toggleStyleClass(treeTable, STRIPED)
);
var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener(
(obs, old, val) -> toggleStyleClass(treeTable, Tweaks.ALT_ICON)
);
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener(
(obs, old, value) -> toggleStyleClass(treeTable, Tweaks.EDGE_TO_EDGE)
);
var footer = new HBox(
HGAP_20, borderToggle, denseToggle, stripeToggle,
altIconToggle, edge2edgeToggle
);
footer.setAlignment(Pos.CENTER);
// == TREE TABLE ==
var rootVal = Product.empty(0);
rootVal.setBrand("Root");
var root = new TreeItem<>(rootVal);
for (int idx = 1; idx <= FAKER.random().nextInt(5, 10); idx++) {
String brand = FAKER.commerce().brand();
var groupVal = Product.empty(0);
groupVal.setBrand(brand);
var group = new TreeItem<>(groupVal);
group.getChildren().setAll(
createTreeItems(idx * 100, FAKER.random().nextInt(5, 10), brand)
);
root.getChildren().add(group);
}
treeTable = createTreeTable();
treeTable.setColumnResizePolicy(TreeTableView.UNCONSTRAINED_RESIZE_POLICY);
treeTable.setRoot(root);
VBox.setVgrow(treeTable, Priority.ALWAYS);
// == HEADER ==
var header = new HBox(createPropertiesMenu(treeTable));
header.setAlignment(Pos.CENTER_LEFT);
// ~
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]TreeTableView[/i] features \
and also serves as an object for monkey testing."""
);
var playground = new VBox(VGAP_10, description, header, treeTable, footer);
playground.setMinHeight(500);
return playground;
}
private List<TreeItem<Product>> createTreeItems(int startId, int count, String brand) {
return IntStream.range(startId, startId + count + 1).boxed()
.map(id -> Product.random(id, brand, FAKER))
.map(TreeItem::new)
.toList();
}
@SuppressWarnings("unchecked")
private TreeTableView<Product> createTreeTable() {
var arrowCol = new TreeTableColumn<Product, String>("#");
// This is placeholder column for disclosure nodes. We need to fill it
// with empty strings or each .tree-table-cell will be marked as :empty,
// which in turn leads to absent borders.
arrowCol.setCellValueFactory(cell -> new SimpleStringProperty(""));
arrowCol.setMinWidth(50);
arrowCol.setMaxWidth(50);
var stateCol = new TreeTableColumn<Product, Boolean>("State");
stateCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("state"));
stateCol.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(stateCol));
stateCol.setEditable(true);
var idCol = new TreeTableColumn<Product, String>("ID");
idCol.setCellValueFactory(cell -> {
Product product = cell.getValue().getValue();
return new SimpleStringProperty(
product != null && product.getId() != 0 ? String.valueOf(product.getId()) : ""
);
});
idCol.setEditable(false);
idCol.setMinWidth(80);
var brandCol = new TreeTableColumn<Product, String>("Brand");
brandCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("brand"));
brandCol.setCellFactory(ChoiceBoxTreeTableCell.forTreeTableColumn(
generate(() -> FAKER.commerce().brand(), 10).toArray(String[]::new)
));
brandCol.setEditable(true);
var nameCol = new TreeTableColumn<Product, String>("Name");
nameCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("name"));
nameCol.setCellFactory(ComboBoxTreeTableCell.forTreeTableColumn(
generate(() -> FAKER.commerce().productName(), 10).toArray(String[]::new)
));
nameCol.setEditable(true);
nameCol.setMinWidth(200);
var priceCol = new TreeTableColumn<Product, String>("Price");
priceCol.setCellValueFactory(new TreeItemPropertyValueFactory<>("price"));
priceCol.setCellFactory(TextFieldTreeTableCell.forTreeTableColumn());
priceCol.setEditable(true);
var table = new TreeTableView<Product>();
table.getColumns().setAll(
arrowCol, stateCol, brandCol, idCol, nameCol, priceCol
);
return table;
}
private MenuButton createPropertiesMenu(TreeTableView<Product> treeTable) {
final var resizePolCaption = new CaptionMenuItem("Resize Policy");
final var resizePolicyGroup = new ToggleGroup();
resizePolicyGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof Callback<?, ?> policy) {
//noinspection rawtypes,unchecked
treeTable.setColumnResizePolicy((Callback<TreeTableView.ResizeFeatures, Boolean>) policy);
}
});
final var unconResizeItem = new RadioMenuItem("Unconstrained");
unconResizeItem.setToggleGroup(resizePolicyGroup);
unconResizeItem.setUserData(TreeTableView.UNCONSTRAINED_RESIZE_POLICY);
unconResizeItem.setSelected(true);
final var conResizeItem = new RadioMenuItem("Constrained");
conResizeItem.setToggleGroup(resizePolicyGroup);
conResizeItem.setUserData(TreeTableView.CONSTRAINED_RESIZE_POLICY);
// ~
final var selModeCaption = new CaptionMenuItem("Selection Mode");
final var selModeGroup = new ToggleGroup();
selModeGroup.selectedToggleProperty().addListener((obs, old, val) -> {
if (val != null && val.getUserData() instanceof SelectionMode mode) {
treeTable.getSelectionModel().setSelectionMode(mode);
}
});
final var singleSelItem = new RadioMenuItem("Single");
singleSelItem.setToggleGroup(selModeGroup);
singleSelItem.setUserData(SelectionMode.SINGLE);
final var multiSelItem = new RadioMenuItem("Multiple");
multiSelItem.setToggleGroup(selModeGroup);
multiSelItem.setUserData(SelectionMode.MULTIPLE);
multiSelItem.setSelected(true);
// ~
final var alignLeftItem = new RadioMenuItem("Left");
alignLeftItem.setSelected(true);
alignLeftItem.setOnAction(e -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
c.getStyleClass().removeAll(ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT);
}
});
final var alignCenterItem = new RadioMenuItem("Center");
alignCenterItem.setOnAction(e -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
addStyleClass(c, ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT);
}
});
final var alignRightItem = new RadioMenuItem("Right");
alignRightItem.setOnAction(e -> {
for (TreeTableColumn<?, ?> c : treeTable.getColumns()) {
addStyleClass(c, ALIGN_RIGHT, ALIGN_LEFT, ALIGN_CENTER);
}
});
final var alignItem = new Menu("Alignment");
alignItem.getItems().setAll(alignLeftItem, alignCenterItem, alignRightItem);
final var alignGroup = new ToggleGroup();
alignGroup.getToggles().setAll(alignLeftItem, alignCenterItem, alignRightItem);
// ~
final var showRootItem = new CheckMenuItem("Show root");
treeTable.showRootProperty().bind(showRootItem.selectedProperty());
final var editItem = new CheckMenuItem("Editable");
treeTable.editableProperty().bind(editItem.selectedProperty());
editItem.setSelected(true);
final var cellSelItem = new CheckMenuItem("Enable cell selection");
treeTable.getSelectionModel().cellSelectionEnabledProperty().bind(cellSelItem.selectedProperty());
cellSelItem.setSelected(false);
// ~
final var menuBtnItem = new CheckMenuItem("Show menu button");
treeTable.tableMenuButtonVisibleProperty().bind(menuBtnItem.selectedProperty());
menuBtnItem.setSelected(true);
final var propsMenu = new MenuButton("Properties");
propsMenu.getItems().setAll(
resizePolCaption,
unconResizeItem,
conResizeItem,
selModeCaption,
singleSelItem,
multiSelItem,
new SeparatorMenuItem(),
alignItem,
showRootItem,
editItem,
cellSelItem,
menuBtnItem
);
return propsMenu;
}
private void addStyleClass(TreeTableColumn<?, ?> c,
String styleClass,
String... excludes) {
Objects.requireNonNull(c);
Objects.requireNonNull(styleClass);
if (excludes != null && excludes.length > 0) {
c.getStyleClass().removeAll(excludes);
}
c.getStyleClass().add(styleClass);
}
}

@ -0,0 +1,542 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.io.File;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.control.cell.ChoiceBoxTreeCell;
import javafx.scene.control.cell.ComboBoxTreeCell;
import javafx.scene.control.cell.TextFieldTreeCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class TreeViewPage extends OutlinePage {
public static final String NAME = "TreeView";
@Override
public String getName() {
return NAME;
}
public TreeViewPage() {
super();
addFormattedText("""
The [i]TreeView[/i] provides a way to present tree structures. A tree has \
a root node which contains all the hierarchical values."""
);
addSection("Usage", usageExample());
addSection("Dense", denseExample());
addSection("Alt Icon", altIconExample());
addSection("Edge-to-Edge", edge2EdgeExample());
addSection("Playground", playground());
}
@SuppressWarnings("unchecked")
private ExampleBox usageExample() {
//snippet_1:start
var item1 = new TreeItem<>("Programming", icon("code"));
item1.getChildren().setAll(
new TreeItem<>("Kotlin", icon("kotlin")),
new TreeItem<>("Python", icon("python")),
new TreeItem<>("C++", icon("cplusplus"))
);
var item2 = new TreeItem<>("Databases", icon("database"));
item2.getChildren().setAll(
new TreeItem<>("PostgreSQL", icon("postgresql")),
new TreeItem<>("Redis", icon("redis")),
new TreeItem<>("MongoDB", icon("mongodb"))
);
var item3 = new TreeItem<>("Cloud", icon("cloud"));
item3.getChildren().setAll(
new TreeItem<>("Terraform", icon("terraform")),
new TreeItem<>("Azure", icon("azure")),
new TreeItem<>("Kubernetes", icon("kubernetes"))
);
var root = new TreeItem<>("Technologies");
root.setExpanded(true);
root.getChildren().addAll(item1, item2, item3);
var tree = new TreeView<>(root);
//snippet_1:end
tree.setMaxWidth(Double.MAX_VALUE);
tree.setMinHeight(250);
HBox.setHgrow(tree, Priority.ALWAYS);
var box = new HBox(tree);
var description = BBCodeParser.createFormattedText("""
In each tree the highest object in the hierarchy is called the "root". \
The root contains several child items, which can have children as well. \
An item without children is called "leaf".""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox denseExample() {
//snippet_2:start
class Helper {
public static TreeItem<String> scan(File dir, int depth) {
var parent = new TreeItem<>(
dir.getName(),
new FontIcon(Feather.FOLDER)
);
File[] files = dir.listFiles();
depth--;
if (files != null) {
for (File f : files) {
if (f.isDirectory() && depth > 0) {
parent.getChildren().add(scan(f, depth));
} else {
var leaf = new TreeItem<>(
f.getName(),
new FontIcon(Feather.FILE)
);
parent.getChildren().add(leaf);
}
}
parent.getChildren().sort(
Comparator.comparing(TreeItem::getValue)
);
}
return parent;
}
}
var root = Helper.scan(new File(System.getProperty("user.home")), 3);
var tree = new TreeView<>(root);
tree.getStyleClass().add(Styles.DENSE);
tree.setShowRoot(false);
//snippet_2:end
tree.setMaxWidth(Double.MAX_VALUE);
tree.setMinHeight(350);
HBox.setHgrow(tree, Priority.ALWAYS);
var box = new HBox(tree);
var description = BBCodeParser.createFormattedText(
"The [i]TreeView[/i] rows can be made more compact by cutting tree cell padding."
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
@SuppressWarnings("unchecked")
private ExampleBox altIconExample() {
//snippet_3:start
var item1 = new TreeItem<>("Algorithms");
item1.getChildren().setAll(
new TreeItem<>("Bubble Sort"),
new TreeItem<>("Selection Sort"),
new TreeItem<>("Merge Sort")
);
var item2 = new TreeItem<>("Data Structures");
item2.getChildren().setAll(
new TreeItem<>("Stack"),
new TreeItem<>("Queue"),
new TreeItem<>("Linked List")
);
var item3 = new TreeItem<>("Artificial Intelligence");
item3.getChildren().setAll(
new TreeItem<>("Machine Learning"),
new TreeItem<>("Natural Language Processing"),
new TreeItem<>("Robotics")
);
var root = new TreeItem<>("Computer Science");
root.setExpanded(true);
root.getChildren().addAll(item1, item2, item3);
var tree = new TreeView<>(root);
tree.getStyleClass().add(Tweaks.ALT_ICON);
//snippet_3:end
tree.setMaxWidth(Double.MAX_VALUE);
tree.setMinHeight(250);
HBox.setHgrow(tree, Priority.ALWAYS);
var box = new HBox(tree);
var description = BBCodeParser.createFormattedText("""
There's additional tweak [code]Tweaks.ALT_ICON[/code] to change the [i]TreeView[/i] \
arrow icon."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
@SuppressWarnings("unchecked")
private ExampleBox edge2EdgeExample() {
//snippet_4:start
var item1 = new TreeItem<>("Action");
item1.getChildren().setAll(
new TreeItem<>("Grand Theft Auto V"),
new TreeItem<>("Call of Duty: Modern Warfare"),
new TreeItem<>("Uncharted 4: A Thief's End")
);
var item2 = new TreeItem<>("Adventure");
item2.getChildren().setAll(
new TreeItem<>("The Legend of Zelda: Breath of the Wild"),
new TreeItem<>("Tomb Raider"),
new TreeItem<>("Minecraft")
);
var item3 = new TreeItem<>("Role-playing");
item3.getChildren().setAll(
new TreeItem<>("The Elder Scrolls V: Skyrim"),
new TreeItem<>("Fallout 4"),
new TreeItem<>("Final Fantasy XV")
);
var root = new TreeItem<>("Games");
root.setExpanded(true);
root.getChildren().addAll(item1, item2, item3);
var tree = new TreeView<>(root);
//snippet_4:end
tree.setMaxWidth(Double.MAX_VALUE);
tree.setMinHeight(250);
HBox.setHgrow(tree, Priority.ALWAYS);
var box = new HBox(tree);
box.setStyle("""
-fx-border-color: -color-accent-emphasis;
-fx-border-width: 2px;"""
);
var description = BBCodeParser.createFormattedText("""
Use [code]Tweaks.EDGE_TO_EDGE[/code] style class to remove the [i]TreeView[/i] outer borders. \
This is useful if you want to place the table into external container that already has its \
own borders."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
private ImageView icon(String name) {
var img = new Image(Resources.getResourceAsStream("images/devicons/" + name + ".png"));
return new ImageView(img);
}
///////////////////////////////////////////////////////////////////////////
// Playground //
///////////////////////////////////////////////////////////////////////////
private static final int MAX_TREE_DEPTH = 3;
private static final int[] TREE_DICE = {-1, 0, 1};
private final BorderPane treeWrapper = new BorderPane();
private final ComboBox<Example> exampleSelect = createExampleSelect();
private VBox playground() {
var denseToggle = new ToggleSwitch("Dense");
denseToggle.selectedProperty().addListener((obs, old, value) ->
findDisplayedTree().ifPresent(tv -> Styles.toggleStyleClass(tv, Styles.DENSE))
);
var showRootToggle = new ToggleSwitch("Show root");
showRootToggle.selectedProperty().addListener((obs, old, val) ->
findDisplayedTree().ifPresent(tv -> {
if (val != null) {
tv.setShowRoot(val);
}
})
);
showRootToggle.setSelected(true);
var altIconToggle = new ToggleSwitch("Alt icon");
altIconToggle.selectedProperty().addListener((obs, old, val) ->
findDisplayedTree().ifPresent(tv -> Styles.toggleStyleClass(tv, Tweaks.ALT_ICON))
);
var edge2edgeToggle = new ToggleSwitch("Edge to edge");
edge2edgeToggle.selectedProperty().addListener((obs, old, val) ->
findDisplayedTree().ifPresent(tv -> Styles.toggleStyleClass(tv, Tweaks.EDGE_TO_EDGE))
);
var toggles = new HBox(VGAP_20, denseToggle, showRootToggle, altIconToggle, edge2edgeToggle);
toggles.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
The playground demonstrates the most important [i]TreeView[/i] features \
and also serves as an object for monkey testing."""
);
VBox.setVgrow(treeWrapper, Priority.ALWAYS);
var playground = new VBox(
VGAP_10,
description,
new Label("Select an example:"),
exampleSelect,
treeWrapper,
toggles
);
playground.setMinHeight(500);
return playground;
}
private ComboBox<Example> createExampleSelect() {
var select = new ComboBox<Example>();
select.setMaxWidth(Double.MAX_VALUE);
select.getItems().setAll(Example.values());
select.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
TreeView<String> newTree = createTree(val);
// copy existing style classes and properties to the new tree
findDisplayedTree().ifPresent(tv -> {
List<String> currentStyles = tv.getStyleClass();
currentStyles.remove("tree-view");
newTree.getStyleClass().addAll(currentStyles);
newTree.setShowRoot(tv.isShowRoot());
newTree.setDisable(tv.isDisable());
});
treeWrapper.setCenter(newTree);
});
select.setConverter(new StringConverter<>() {
@Override
public String toString(Example example) {
return example == null ? "" : example.getName();
}
@Override
public Example fromString(String s) {
return Example.find(s);
}
});
return select;
}
@Override
protected void onRendered() {
super.onRendered();
exampleSelect.getSelectionModel().selectFirst();
}
private Optional<TreeView<?>> findDisplayedTree() {
return treeWrapper.getChildren().size() > 0
? Optional.of((TreeView<?>) treeWrapper.getChildren().get(0))
: Optional.empty();
}
private TreeView<String> createTree(Example example) {
switch (example) {
case TEXT -> {
return stringTree();
}
case GRAPHIC -> {
return graphicTree();
}
case EDITABLE -> {
return editableTree();
}
case CHECK_BOX -> {
return checkBoxTree();
}
case CHOICE_BOX -> {
return choiceBoxTree();
}
case COMBO_BOX -> {
return comboBoxTree();
}
default -> throw new IllegalArgumentException("Unexpected enum value: " + example);
}
}
private <T> void generateTree(TreeItem<T> parent,
Supplier<TreeItem<T>> itemFactory,
int childLimit,
int depth) {
if (childLimit == 0) {
return;
}
var item = itemFactory.get();
parent.getChildren().add(item);
TreeItem<T> nextParent = parent; // sibling
int nextDepth = depth;
int rand = TREE_DICE[RANDOM.nextInt(TREE_DICE.length)];
if (rand < 0 && parent.getParent() != null) { // go up
nextParent = parent.getParent();
nextDepth = --depth;
}
if (rand > 0 && depth < MAX_TREE_DEPTH) { // go down
nextParent = item;
nextDepth = ++depth;
}
generateTree(nextParent, itemFactory, --childLimit, nextDepth);
}
private TreeView<String> stringTree() {
var root = new TreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
generateTree(root, () -> new TreeItem<>(
FAKER.internet().domainWord()), 30, 1
);
tree.setRoot(root);
return tree;
}
private TreeView<String> graphicTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
generateTree(root, () -> new TreeItem<>(
FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1
);
tree.setRoot(root);
return tree;
}
private TreeView<String> editableTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(TextFieldTreeCell.forTreeView());
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(
FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1
);
tree.setRoot(root);
return tree;
}
// Note that CheckBoxTreeCell is incompatible with user graphic,
// because it adds graphic inside .checkbox container. #javafx-bug
private TreeView<String> checkBoxTree() {
var root = new CheckBoxTreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(CheckBoxTreeCell.forTreeView());
generateTree(root, () -> new CheckBoxTreeItem<>(
FAKER.internet().domainWord()), 30, 1
);
tree.setRoot(root);
return tree;
}
private TreeView<String> choiceBoxTree() {
var root = new TreeItem<>("root");
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(ChoiceBoxTreeCell.forTreeView(
generate(() -> FAKER.internet().domainWord(), 10).toArray(String[]::new)
));
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(
FAKER.internet().domainWord()), 30, 1
);
tree.setRoot(root);
return tree;
}
private TreeView<String> comboBoxTree() {
var root = new TreeItem<>("root", new FontIcon(Feather.FOLDER));
root.setExpanded(true);
var tree = new TreeView<String>();
tree.setCellFactory(ComboBoxTreeCell.forTreeView(
generate(() -> FAKER.internet().domainWord(), 10).toArray(String[]::new)
));
tree.setEditable(true);
generateTree(root, () -> new TreeItem<>(
FAKER.internet().domainWord(), new FontIcon(Feather.FILE)), 30, 1
);
tree.setRoot(root);
return tree;
}
private enum Example {
TEXT("Text"),
GRAPHIC("Text with icons"),
EDITABLE("TextFieldTreeCell"),
CHECK_BOX("CheckBoxTreeCell"),
CHOICE_BOX("ChoiceBoxTreeCell"),
COMBO_BOX("ComboBoxTreeCell");
private final String name;
Example(String name) {
this.name = name;
}
public String getName() {
return name;
}
public static Example find(String name) {
return Arrays.stream(Example.values())
.filter(example -> Objects.equals(example.getName(), name))
.findFirst()
.orElse(null);
}
}
}

@ -1,19 +1,18 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.components; package atlantafx.sampler.page.extras;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.OutlinePage;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; import javafx.scene.text.TextFlow;
public class BBCodePage extends AbstractPage { public class BBCodePage extends OutlinePage {
public static final String NAME = "BBCode Markup"; public static final String NAME = "BBCode Markup";
@ -25,59 +24,59 @@ public class BBCodePage extends AbstractPage {
public BBCodePage() { public BBCodePage() {
super(); super();
setUserContent(new VBox(20, addFormattedText("""
reference(), BBCode (Bulletin Board Code) is a lightweight markup language used to \
article() format messages in many Internet forum software. The available tags of \
)); BBCode are indicated by square brackets surrounding a keyword, and are \
parsed before being translated into [s]HTML[/s] JavaFX layout :)""");
addSection("Text Type", textTypeReference());
addSection("Text Style", textStyleReference());
addSection("Subscript and Superscript", subscriptReference());
addSection("Headings", headingReference());
addSection("Links", linksReference());
addSection("Lists", listsReference());
addSection("Alignment", alignmentReference());
addSection("Indentation", indentReference());
addSection("Abbreviation", abbreviationReference());
addSection("Separator", hrReference());
addSection("Test Article", article());
} }
private VBox reference() { private ReferenceBlock textTypeReference() {
var root = new VBox(20);
root.setAlignment(Pos.TOP_LEFT);
var header = BBCodeParser.createFormattedText("""
[left][heading=1]Reference[/heading][/left]\
BBCode ("Bulletin Board Code") is a lightweight markup language used to format messages \
in many Internet forum software. It was first introduced in 1998. The available tags of BBCode \
are usually indicated by square brackets surrounding a keyword, and are parsed before \
being translated into [s]HTML[/s] JavaFX layout.""");
root.getChildren().add(header);
ReferenceBlock block = new ReferenceBlock( ReferenceBlock block = new ReferenceBlock(
"Bold, italics, underline and strikethrough",
"Makes the wrapped text bold, italic, underlined, or strikethrough." "Makes the wrapped text bold, italic, underlined, or strikethrough."
); );
block.addFormattedText("This is [b]bold[/b] text."); block.addFormattedText("This is [b]bold[/b] text.");
block.addFormattedText("This is [i]italic[/i] text."); block.addFormattedText("This is [i]italic[/i] text.");
block.addFormattedText("This is [u]underlined[/u] text."); block.addFormattedText("This is [u]underlined[/u] text.");
block.addFormattedText("This is [s]strikethrough[/s] text."); block.addFormattedText("This is [s]strikethrough[/s] text.");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock textStyleReference() {
"Text color, font and size", ReferenceBlock block = new ReferenceBlock("Changes the color, font, or size of the wrapped text.");
"Changes the color, font, or size of the wrapped text."
);
block.addFormattedText("This is [color=red]red[/color] text."); block.addFormattedText("This is [color=red]red[/color] text.");
block.addFormattedText("This is [color=-color-accent-emphasis]accent[/color] text."); block.addFormattedText("This is [color=-color-accent-emphasis]accent[/color] text.");
block.addFormattedText("This is [label style='-fx-background-color: yellow']background[/label] color."); block.addFormattedText(
"This is [label style='-fx-background-color: -color-warning-muted']background[/label] color.");
block.addFormattedText("This is [font=monospace]monospaced[/font] font."); block.addFormattedText("This is [font=monospace]monospaced[/font] font.");
block.addFormattedText("This is a [code]public[/code] Java keyword.");
block.addFormattedText("This is [small]small[/small] and [size=1.5em]big[/size] text."); block.addFormattedText("This is [small]small[/small] and [size=1.5em]big[/size] text.");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock subscriptReference() {
"Subscript and superscript", ReferenceBlock block = new ReferenceBlock(
"A text that is set slightly below or above the normal line of type, respectively." "A text that is set slightly below or above the normal line of type, respectively."
); );
block.addLayout("log[sub][small]2[/small][/sub](256) = 8"); block.addLayout("log[sub][small]2[/small][/sub](256) = 8");
block.addLayout("10[sup][small]2[/small][/sup] = 100"); block.addLayout("10[sup][small]2[/small][/sup] = 100");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock headingReference() {
"Headings levels 1 to 5", ReferenceBlock block = new ReferenceBlock("Marks text as a structured heading.");
"Marks text as a structured heading."
);
block.addFormattedText(""" block.addFormattedText("""
[heading=1]H1 headline[/heading] [heading=1]H1 headline[/heading]
@ -98,12 +97,11 @@ public class BBCodePage extends AbstractPage {
[caption]Caption[/caption] [caption]Caption[/caption]
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non purus a nisi ornare facilisis."""); Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non purus a nisi ornare facilisis.""");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock listsReference() {
"Lists", ReferenceBlock block = new ReferenceBlock("Displays a bulleted or numbered list.");
"Displays a bulleted or numbered list."
);
block.addLayout(""" block.addLayout("""
[ul] [ul]
[li]Entry 1[/li] [li]Entry 1[/li]
@ -129,56 +127,51 @@ public class BBCodePage extends AbstractPage {
[li]Entry 1[/li] [li]Entry 1[/li]
[li]Entry 2[/li] [li]Entry 2[/li]
[/ol]"""); [/ol]""");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock linksReference() {
"Linking", ReferenceBlock block = new ReferenceBlock(
"Links the wrapped text to the specified web page or email address." "Links the wrapped text to the specified web page or email address."
); );
block.addFormattedText("[url=https://www.example.com]Go to example.com[/url]"); block.addFormattedText("[url=https://www.example.com]Go to example.com[/url]");
block.addFormattedText("[email=johndoe@example.com]Email me[/email]"); block.addFormattedText("[email=johndoe@example.com]Email me[/email]");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock alignmentReference() {
"Alignment", ReferenceBlock block = new ReferenceBlock("Changes the alignment of the wrapped text.");
"Changes the alignment of the wrapped text."
);
block.addLayout("[left]Left-aligned[/left]"); block.addLayout("[left]Left-aligned[/left]");
block.addLayout("[center]Center-aligned[/center]"); block.addLayout("[center]Center-aligned[/center]");
block.addLayout("[right]Right-aligned[/right]"); block.addLayout("[right]Right-aligned[/right]");
block.addLayout("[align=center]Center-aligned[/align]"); block.addLayout("[align=center]Center-aligned[/align]");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock indentReference() {
"Text indent", ReferenceBlock block = new ReferenceBlock("Indents the wrapped text.");
"Indents the wrapped text."
);
block.addLayout("[indent]Indented text[/indent]"); block.addLayout("[indent]Indented text[/indent]");
block.addLayout("[indent=3]More indented[/indent]"); block.addLayout("[indent=3]More indented[/indent]");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock hrReference() {
"Horizontal line", ReferenceBlock block = new ReferenceBlock("A horizontal separator line.");
"A horizontal separator line."
);
block.addLayout("Default line: [hr/]"); block.addLayout("Default line: [hr/]");
block.addLayout("Thick line: [hr=5/]"); block.addLayout("Thick line: [hr=5/]");
root.getChildren().add(block); return block;
}
block = new ReferenceBlock( private ReferenceBlock abbreviationReference() {
"Abbreviation", ReferenceBlock block = new ReferenceBlock("An abbreviation, with mouse-over expansion.");
"An abbreviation, with mouse-over expansion."
);
block.addLayout("[abbr='on hover text']text[/abbr]"); block.addLayout("[abbr='on hover text']text[/abbr]");
block.addLayout("[abbr]text[/abbr]"); block.addLayout("[abbr]text[/abbr]");
root.getChildren().add(block); return block;
return root;
} }
private VBox article() { private VBox article() {
var article = """ var article = """
[left][heading=1]Example[/heading][/left]\ [left][heading=1]JavaFX - Overview[/heading][/left]\
[b]JavaFX[/b] is a Java library used to build Rich Internet Applications. \ [b]JavaFX[/b] is a Java library used to build Rich Internet Applications. \
The applications written using this library can run consistently across multiple \ The applications written using this library can run consistently across multiple \
@ -208,7 +201,8 @@ public class BBCodePage extends AbstractPage {
full-featured application.[/li]\ full-featured application.[/li]\
[li][b]CSS like Styling[/b] JavaFX provides a CSS like styling. By using this, you can improve \ [li][b]CSS like Styling[/b] JavaFX provides a CSS like styling. By using this, you can improve \
the design of your application with a simple knowledge of CSS.[/li]\ the design of your application with a simple knowledge of CSS.[/li]\
[li][b]Canvas and Printing[/b] JavaFX provides [label style=-fx-background-color:yellow]Canvas[/label], \ [li][b]Canvas and Printing[/b] JavaFX provides \
[label style=-fx-background-color:-color-warning-muted]Canvas[/label], \
an immediate mode style of rendering API. Within the package [font=monospace]javafx.scene.canvas[/font] \ an immediate mode style of rendering API. Within the package [font=monospace]javafx.scene.canvas[/font] \
it holds a set of classes for canvas, using which we can draw directly within an area of the \ it holds a set of classes for canvas, using which we can draw directly within an area of the \
JavaFX scene. JavaFX also provides classes for Printing purposes in the package javafx.print.[/li]\ JavaFX scene. JavaFX also provides classes for Printing purposes in the package javafx.print.[/li]\
@ -231,10 +225,10 @@ public class BBCodePage extends AbstractPage {
private final VBox leftBox; private final VBox leftBox;
private final VBox rightBox; private final VBox rightBox;
public ReferenceBlock(String title, String description) { public ReferenceBlock(String description) {
super(); super();
var titleLabel = new Label(title); var titleLabel = new Label(description);
titleLabel.getStyleClass().add(Styles.TITLE_4); titleLabel.getStyleClass().add(Styles.TITLE_4);
leftBox = new VBox(15); leftBox = new VBox(15);
@ -260,7 +254,7 @@ public class BBCodePage extends AbstractPage {
splitBox.setSpacing(20); splitBox.setSpacing(20);
setSpacing(10); setSpacing(10);
getChildren().addAll(titleLabel, new Label(description), splitBox); getChildren().addAll(new Label(description), splitBox);
} }
public void addFormattedText(String markup) { public void addFormattedText(String markup) {

@ -0,0 +1,182 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.Breadcrumbs;
import atlantafx.base.controls.Breadcrumbs.BreadCrumbItem;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class BreadcrumbsPage extends OutlinePage {
public static final String NAME = "Breadcrumbs";
@Override
public String getName() {
return NAME;
}
public BreadcrumbsPage() {
super();
addFormattedText("""
Represents a bread crumb bar. This control is useful to visualize and navigate \
a hierarchical path structure, such as file systems."""
);
addSection("Usage", usageExample());
addSection("Custom Item", customItemExample());
addSection("Custom Divider", customDividerExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var items = generate(() -> FAKER.science().element(), 4);
BreadCrumbItem<String> root = Breadcrumbs.buildTreeModel(
items.toArray(String[]::new)
);
var crumbs = new Breadcrumbs<>(root);
crumbs.setSelectedCrumb(getTreeItemByIndex(root, 2));
//snippet_1:end
var nextBtn = new Button("Next");
nextBtn.getStyleClass().addAll(Styles.ACCENT);
nextBtn.setOnAction(e -> {
var selected = crumbs.getSelectedCrumb();
if (selected.getChildren().size() > 0) {
var next = selected.getChildren().get(0);
crumbs.setSelectedCrumb((BreadCrumbItem<String>) next);
}
});
crumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
nextBtn.setDisable(val.getChildren().isEmpty());
});
var box = new HBox(40, nextBtn, crumbs);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The [i]BreadCrumbs[/i] uses the [i]TreeItem[/i] API to maintain its model. \
You can create a tree model from a flat list by using the static \
[code]buildTreeModel()[/code] method."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox customItemExample() {
//snippet_2:start
var items = generate(() -> FAKER.science().element(), 4);
BreadCrumbItem<String> root = Breadcrumbs.buildTreeModel(
items.toArray(String[]::new)
);
var crumbs = new Breadcrumbs<>(root);
crumbs.setCrumbFactory(crumb -> {
var btn = new Button(crumb.getValue(), new FontIcon(randomIcon()));
btn.getStyleClass().add(Styles.FLAT);
btn.setFocusTraversable(false);
return btn;
});
crumbs.setSelectedCrumb(getTreeItemByIndex(root, 2));
//snippet_2:end
var nextBtn = new Button("Next");
nextBtn.getStyleClass().addAll(Styles.ACCENT);
nextBtn.setOnAction(e -> {
var selected = crumbs.getSelectedCrumb();
if (selected.getChildren().size() > 0) {
var next = selected.getChildren().get(0);
crumbs.setSelectedCrumb((BreadCrumbItem<String>) next);
}
});
crumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
nextBtn.setDisable(val.getChildren().isEmpty());
});
var box = new HBox(40, nextBtn, crumbs);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
By default, the [i]Breadcrumbs[/i] uses [i]Hyperlink[/i] to represent \
its items. If you want to use a different kind of item, you can provide \
a [code]crumbFactory[/code] to insert an arbitrary [i]Node[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox customDividerExample() {
//snippet_3:start
var items = generate(() -> FAKER.science().element(), 4);
BreadCrumbItem<String> root = Breadcrumbs.buildTreeModel(
items.toArray(String[]::new)
);
var crumbs = new Breadcrumbs<>(root);
crumbs.setDividerFactory(item -> {
if (item == null) {
return new Label("", new FontIcon(Material2AL.HOME));
}
return !item.isLast()
? new Label("", new FontIcon(Material2AL.CHEVRON_RIGHT))
: null;
});
crumbs.setSelectedCrumb(getTreeItemByIndex(root, 2));
//snippet_3:end
var nextBtn = new Button("Next");
nextBtn.getStyleClass().addAll(Styles.ACCENT);
nextBtn.setOnAction(e -> {
var selected = crumbs.getSelectedCrumb();
if (selected.getChildren().size() > 0) {
var next = selected.getChildren().get(0);
crumbs.setSelectedCrumb((BreadCrumbItem<String>) next);
}
});
crumbs.selectedCrumbProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
nextBtn.setDisable(val.getChildren().isEmpty());
});
var box = new HBox(40, nextBtn, crumbs);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
Similarly, you can customize the [i]Breadcrumbs[/i] divider by \
providing a [code]dividerFactory[/code]."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private <T> BreadCrumbItem<T> getTreeItemByIndex(BreadCrumbItem<T> node, int index) {
var counter = index;
var current = node;
while (counter > 0 && current.getParent() != null) {
current = (BreadCrumbItem<T>) current.getParent();
counter--;
}
return current;
}
}

@ -0,0 +1,167 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.InlineDatePicker;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.theme.CSSFragment;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.scene.control.DateCell;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
public class CalendarPage extends OutlinePage {
public static final String NAME = "Calendar";
private static final LocalDate TODAY = LocalDate.now(ZoneId.systemDefault());
@Override
public String getName() {
return NAME;
}
public CalendarPage() {
super();
addFormattedText("""
The date picker control that allows the user to select a date. Unlike standard JavaFX \
[font=monospace]javafx.scene.control.DatePicker[/font] the [i]Calendar[/i] is not concealed \
within a popup window."""
);
addSection("Usage", usageExample());
addSection("No past dates", noPastDatesExample());
addSection("User slots", clockExample());
addSection("Style", styleExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var dp = new InlineDatePicker(TODAY);
dp.setShowWeekNumbers(true);
//snippet_1:end
var box = new HBox(dp);
var description = BBCodeParser.createFormattedText("""
In the default state, no date is selected. You can modify this behavior \
either by using the constructor or by utilizing the [font=monospace]setValue()[/font] \
method.""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox noPastDatesExample() {
//snippet_2:start
class FutureDateCell extends DateCell {
@Override
public void updateItem(LocalDate date, boolean empty) {
super.updateItem(date, empty);
setDisable(empty || date.isBefore(TODAY));
}
}
var dp = new InlineDatePicker(TODAY);
dp.setDayCellFactory(c -> new FutureDateCell());
//snippet_2:end
var box = new HBox(dp);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how you can disable past dates in the [i]Calendar[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox clockExample() {
final var style = """
-fx-border-width: 0 0 0.5 0;
-fx-border-color: -color-border-default;""";
//snippet_3:start
class Clock extends VBox {
static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("EEEE, LLLL dd, yyyy");
static final DateTimeFormatter TIME_FORMATTER =
DateTimeFormatter.ofPattern("HH:mm:ss");
public Clock() {
var clockLbl = new Label(TIME_FORMATTER.format(
LocalTime.now(ZoneId.systemDefault()))
);
clockLbl.getStyleClass().add(Styles.TITLE_2);
var dateLbl = new Label(DATE_FORMATTER.format(
LocalDate.now(ZoneId.systemDefault()))
);
// -fx-border-width: 0 0 0.5 0;
// -fx-border-color: -color-border-default;
setStyle(style);
setSpacing(10);
getChildren().setAll(clockLbl, dateLbl);
var t = new Timeline(new KeyFrame(
Duration.seconds(1),
e -> {
var time = LocalTime.now(ZoneId.systemDefault());
clockLbl.setText(TIME_FORMATTER.format(time));
}
));
t.setCycleCount(Animation.INDEFINITE);
t.playFromStart();
}
}
var dp = new InlineDatePicker(TODAY);
dp.setTopNode(new Clock());
dp.setShowWeekNumbers(true);
//snippet_3:end
var box = new HBox(dp);
var description = BBCodeParser.createFormattedText("""
The [i]Calendar[/i] comes equipped with two slots (top and bottom) where \
users can place their own content. For example, you can use these slots to \
display a clock widget on top of the [i]Calendar[/i]."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox styleExample() {
var style = """
.date-picker-popup {
-color-date-border: -color-accent-emphasis;
-color-date-month-year-bg: -color-accent-emphasis;
-color-date-month-year-fg: -color-fg-emphasis;
}""";
//snippet_4:start
var dp = new InlineDatePicker(TODAY);
dp.setShowWeekNumbers(true);
// -color-date-border: -color-accent-emphasis;
// -color-date-month-year-bg: -color-accent-emphasis;
// -color-date-month-year-fg: -color-fg-emphasis;
new CSSFragment(style).addTo(dp);
//snippet_4:end
var box = new HBox(dp);
var description = BBCodeParser.createFormattedText("""
You can alter the style of the [i]Calendar[/i] by using looked-up color variables."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
}

@ -0,0 +1,172 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.MaskTextField;
import atlantafx.base.controls.PasswordTextField;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedAL;
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
public class CustomTextFieldPage extends OutlinePage {
public static final String NAME = "CustomTextField";
@Override
public String getName() {
return NAME;
}
public CustomTextFieldPage() {
super();
addFormattedText("""
A base class for placing nodes inside the text field itself, without being \
on top of the users typed-in text."""
);
addSection("Usage", usageExample());
addSection("Color", colorExample());
addSection("Password", passwordSample());
addSection("Input Mask", inputMaskExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var tf1 = new CustomTextField();
tf1.setPromptText("Prompt text");
tf1.setRight(new FontIcon(Feather.X));
tf1.setPrefWidth(150);
var tf2 = new CustomTextField();
tf2.setPromptText("Prompt text");
tf2.setLeft(new FontIcon(Feather.MAP_PIN));
tf2.setPrefWidth(150);
var tf3 = new CustomTextField("Text");
tf3.setLeft(new FontIcon(Feather.MAP_PIN));
tf3.setRight(new FontIcon(Feather.X));
tf3.setPrefWidth(150);
//snippet_1:end
var box = new HBox(30, tf1, tf2, tf3);
var description = BBCodeParser.createFormattedText("""
You can add arbitrary nodes to the [i]CustomTextField[/i] by setting \
the [code]left[/code] and [code]right[/code] properties, respectively."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox colorExample() {
//snippet_2:start
var tf1 = new CustomTextField("Text");
tf1.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
tf1.setRight(new FontIcon(Feather.X));
tf1.setPrefWidth(150);
var tf2 = new CustomTextField();
tf2.pseudoClassStateChanged(Styles.STATE_DANGER, true);
tf2.setLeft(new FontIcon(Feather.MAP_PIN));
tf2.setPrefWidth(150);
//snippet_2:end
var box = new HBox(30, tf1, tf2);
var description = BBCodeParser.createFormattedText("""
Use [code]Styles.STATE_SUCCESS[/code] or [code]Styles.STATE_DANGER[/code] \
pseudo-classes to change the CustomTextField/i] color. This especially useful to indicate \
the validation result."""
);
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
private ExampleBox passwordSample() {
//snippet_3:start
var tf = new PasswordTextField("qwerty");
tf.setPrefWidth(250);
var icon = new FontIcon(Feather.EYE_OFF);
icon.setCursor(Cursor.HAND);
icon.setOnMouseClicked(e -> {
icon.setIconCode(tf.isRevealPassword()
? Feather.EYE_OFF : Feather.EYE
);
tf.setRevealPassword(!tf.isRevealPassword());
});
tf.setRight(icon);
//snippet_3:end
var box = new HBox(30, tf);
var description = BBCodeParser.createFormattedText("""
The [i]PasswordTextField[/i] is a variant of [i]CustomTextField[/i] that allows users \
to input passwords. Unlike the standard JavaFX [i]PasswordField[/i], the content of \
the [i]PasswordTextField[/i] can be revealed."""
);
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox inputMaskExample() {
//snippet_4:start
var phoneField = new MaskTextField("(999) 999 99 99");
phoneField.setPromptText("(999) 999 99 99");
phoneField.setLeft(new FontIcon(Material2OutlinedMZ.PHONE));
phoneField.setPrefWidth(180);
var cardField = new MaskTextField("9999-9999-9999-9999");
cardField.setLeft(new FontIcon(Material2OutlinedAL.CREDIT_CARD));
cardField.setPrefWidth(200);
var timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
var timeField = new MaskTextField("29:59");
timeField.setText(
LocalTime.now(ZoneId.systemDefault()).format(timeFormatter)
);
timeField.setLeft(new FontIcon(Material2OutlinedMZ.TIMER));
timeField.setPrefWidth(120);
timeField.textProperty().addListener((obs, old, val) -> {
if (val != null) {
try {
//noinspection ResultOfMethodCallIgnored
LocalTime.parse(val, timeFormatter);
timeField.pseudoClassStateChanged(Styles.STATE_DANGER, false);
} catch (DateTimeParseException e) {
timeField.pseudoClassStateChanged(Styles.STATE_DANGER, true);
}
}
});
//snippet_4:end
var box = new HBox(
HGAP_20,
new VBox(5, new Label("Phone Number"), phoneField),
new VBox(5, new Label("Bank Card"), cardField),
new VBox(5, new Label("Time"), timeField)
);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The [i]MaskTextField[/i] allows to restrict user input by applying a \
position-based mask. This is useful for editing cases where the input \
string has a fixed length and each character can be restricted based on its position."""
);
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
}

@ -0,0 +1,161 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.layout.DeckPane;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.AbstractPage;
import java.util.function.Supplier;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ;
public class DeckPanePage extends AbstractPage {
public static final String NAME = "DeckPane";
@Override
public String getName() {
return NAME;
}
public DeckPanePage() {
super();
addFormattedText("""
[i]DeckPane[/i] represents a pane that displays all of its child nodes in a deck, \
where only one node can be visible at a time. It does not maintain any sequence \
(model), but only cares about the top node, which can be changed by various \
transition effects.
Using the control is as simple as calling the [code]swipeX(Node)[/code] and \
[code]slideX(Node)[/code] methods, where [code]X[/code] represents the direction \
of the transition that you want to achieve.""");
addNode(createGallery());
}
private Node createGallery() {
var galleryPane = new BorderPane();
galleryPane.setMinSize(570, 400);
galleryPane.setMaxSize(570, 400);
var image1 = new ImageView(new Image(
Resources.getResourceAsStream("images/gallery/kush-dwivedi-unsplash.jpg"))
);
image1.setFitWidth(450);
image1.setFitHeight(300);
var image2 = new ImageView(new Image(
Resources.getResourceAsStream("images/gallery/markus-spiske-unsplash.jpg"))
);
image2.setFitWidth(450);
image2.setFitHeight(300);
var image3 = new ImageView(new Image(
Resources.getResourceAsStream("images/gallery/r0m0_4-unsplash.jpg"))
);
image3.setFitWidth(450);
image3.setFitHeight(300);
// ~
var transitionTypeBox = new VBox(VGAP_10);
transitionTypeBox.setAlignment(Pos.CENTER);
var label = new Label("Transition Type");
label.getStyleClass().addAll(Styles.TEXT_CAPTION, Styles.TEXT_MUTED);
var swipeRadio = new RadioButton("Swipe");
swipeRadio.setSelected(true);
swipeRadio.setUserData(-1);
var slideRadio = new RadioButton("Slide");
slideRadio.setUserData(1);
var transitionType = new ToggleGroup();
transitionType.getToggles().setAll(swipeRadio, slideRadio);
var radioBox = new HBox(HGAP_20, swipeRadio, slideRadio);
radioBox.setAlignment(Pos.CENTER);
transitionTypeBox.getChildren().setAll(label, radioBox);
// ~
var deck = new DeckPane(image1, image2, image3);
deck.setMinSize(450, 300);
deck.setMaxSize(450, 300);
deck.setAnimationDuration(Duration.millis(350));
galleryPane.setCenter(deck);
// circularly returns the next item from the deck
Supplier<Node> nextItem = () -> {
var next = (deck.getChildren().indexOf(deck.getTopNode()) + 1)
% deck.getChildren().size();
return deck.getChildren().get(next);
};
var topBtn = new Button("", new FontIcon(Material2MZ.NORTH));
topBtn.setOnAction(e -> {
if ((int) transitionType.getSelectedToggle().getUserData() < 0) {
deck.swipeUp(nextItem.get());
} else {
deck.slideUp(nextItem.get());
}
});
galleryPane.setTop(topBtn);
BorderPane.setAlignment(topBtn, Pos.CENTER);
var rightBtn = new Button("", new FontIcon(Material2AL.EAST));
rightBtn.setOnAction(e -> {
if ((int) transitionType.getSelectedToggle().getUserData() < 0) {
deck.swipeRight(nextItem.get());
} else {
deck.slideRight(nextItem.get());
}
});
galleryPane.setRight(rightBtn);
BorderPane.setAlignment(rightBtn, Pos.CENTER);
var bottomBtn = new Button("", new FontIcon(Material2MZ.SOUTH));
bottomBtn.setOnAction(e -> {
if ((int) transitionType.getSelectedToggle().getUserData() < 0) {
deck.swipeDown(nextItem.get());
} else {
deck.slideDown(nextItem.get());
}
});
galleryPane.setBottom(bottomBtn);
BorderPane.setAlignment(bottomBtn, Pos.CENTER);
var leftBtn = new Button("", new FontIcon(Material2MZ.WEST));
leftBtn.setOnAction(e -> {
if ((int) transitionType.getSelectedToggle().getUserData() < 0) {
deck.swipeLeft(nextItem.get());
} else {
deck.slideLeft(nextItem.get());
}
});
galleryPane.setLeft(leftBtn);
BorderPane.setAlignment(leftBtn, Pos.CENTER);
// ~
var root = new VBox(VGAP_20, galleryPane, transitionTypeBox);
root.setAlignment(Pos.CENTER);
return root;
}
}

@ -0,0 +1,224 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
public class InputGroupPage extends OutlinePage {
public static final String NAME = "Input Group";
@Override
public String getName() {
return NAME;
}
public InputGroupPage() {
super();
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.""");
addSection("ComboBox", comboBoxExample());
addSection("Button", buttonExample());
addSection("Text Field", textFieldExample());
addSection("MenuButton", menuButtonExample());
addSection("Label", labelExample());
}
private ExampleBox comboBoxExample() {
//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);
//snippet_1:end
var box = new HBox(leftCmb, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
box.setMinWidth(400);
box.setMaxWidth(400);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how a [i]ComboBox[/i] can be combined \
with a [i]TextField[/i].""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox buttonExample() {
//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(
"", new FontIcon(Feather.REFRESH_CW)
);
rightBtn.getStyleClass().addAll(Styles.BUTTON_ICON);
rightBtn.setOnAction(
e -> leftTfd.setText(FAKER.internet().password())
);
rightBtn.getStyleClass().add(Styles.RIGHT_PILL);
//snippet_2:end
var box = new HBox(leftTfd, rightBtn);
box.setAlignment(Pos.CENTER_LEFT);
box.setMinWidth(400);
box.setMaxWidth(400);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how a [i]Button[/i] can be combined \
with a [i]TextField[/i].""");
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
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);
//snippet_3:end
var box = new HBox(leftTfd, centerTfd, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
box.setMinWidth(400);
box.setMaxWidth(400);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how a multiple [i]TextField[/i]'s can be \
combined into a input group.""");
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
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");
spellItem.setOnAction(
e -> rightTfd.setText(FAKER.harryPotter().spell())
);
var characterItem = new MenuItem("Character");
characterItem.setOnAction(
e -> rightTfd.setText(FAKER.harryPotter().character())
);
var locationItem = new MenuItem("Location");
locationItem.setOnAction(
e -> rightTfd.setText(FAKER.harryPotter().location())
);
var leftMenu = new MenuButton("Dropdown");
leftMenu.getItems().addAll(spellItem, characterItem, locationItem);
leftMenu.getStyleClass().add(Styles.LEFT_PILL);
//snippet_4:end
var box = new HBox(leftMenu, rightTfd);
box.setAlignment(Pos.CENTER_LEFT);
box.setMinWidth(400);
box.setMaxWidth(400);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how a [i]MenuButton[/i] can be combined \
with a [i]TextField[/i].""");
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
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 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 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);
//snippet_5:end
sample1.setMinWidth(400);
sample1.setMaxWidth(400);
sample2.setMinWidth(400);
sample2.setMaxWidth(400);
sample3.setMinWidth(400);
sample3.setMaxWidth(400);
var box = new VBox(VGAP_20, sample1, sample2, sample3);
var description = BBCodeParser.createFormattedText("""
This example demonstrates how a [i]Label[/i] can be used \
in combination with various controls.""");
return new ExampleBox(box, new Snippet(getClass(), 5), description);
}
}

@ -0,0 +1,366 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.ModalPane;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Cursor;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class ModalPanePage extends OutlinePage {
public static final String NAME = "Modal Pane";
private final ModalPane modalPane = new ModalPane();
private final ModalPane modalPaneTop = new ModalPane(-15);
private final ModalPane modalPaneTopmost = new ModalPane(-20);
@Override
public String getName() {
return NAME;
}
public ModalPanePage() {
super();
// add modal pane to the root container, which is StackPane
getChildren().addAll(modalPane, modalPaneTop, modalPaneTopmost);
// reset side and transition to reuse a single modal pane between different examples
modalPane.displayProperty().addListener((obs, old, val) -> {
if (!val) {
modalPane.setAlignment(Pos.CENTER);
modalPane.usePredefinedTransitionFactories(null);
}
});
addFormattedText("""
A container for displaying application dialogs ot top of the current scene \
without opening a modal {@link javafx.stage.Stage}. It's a translucent (glass) pane \
that can hold arbitrary content as well as animate its appearance.""");
addSection("Usage", usageExample());
addSection("Content Position", contentPositionExample());
addSection("Persistent", persistentExample());
addSection("Nesting", nestingExample());
addSection("Maximized", maximizedExample());
addSection("Overflowed", overflowedExample());
addSection("Lightbox", lightboxExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var dialog = new Dialog(450, 450);
var openBtn = new Button("Open Dialog");
openBtn.setOnAction(evt -> modalPane.show(dialog));
var closeBtn = new Button("Close");
closeBtn.setOnAction(evt -> modalPane.hide(true));
dialog.getChildren().setAll(closeBtn);
//snippet_1:end
var box = new HBox(openBtn);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
A [i]ModalPane[/i] can hold any content. By default, you just need to call the \
[code]show()[/code] method, which is a convenience method for setting the content \
of the modal pane and triggering its display state at the same time.""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox contentPositionExample() {
//snippet_2:start
var topDialog = new Dialog(-1, 150);
topDialog.getChildren().setAll(new Label(
FAKER.country().name()
));
var openTopBtn = new Button("Top");
openTopBtn.setOnAction(evt -> {
modalPane.setAlignment(Pos.TOP_CENTER);
modalPane.usePredefinedTransitionFactories(Side.TOP);
modalPane.show(topDialog);
});
// ~
var rightDialog = new Dialog(250, -1);
rightDialog.getChildren().setAll(new Label(
FAKER.country().name()
));
var openRightBtn = new Button("Right");
openRightBtn.setOnAction(evt -> {
modalPane.setAlignment(Pos.TOP_RIGHT);
modalPane.usePredefinedTransitionFactories(Side.RIGHT);
modalPane.show(rightDialog);
});
// ~
var bottomDialog = new Dialog(-1, 150);
bottomDialog.getChildren().setAll(new Label(
FAKER.country().name()
));
var openBottomBtn = new Button("Bottom");
openBottomBtn.setOnAction(evt -> {
modalPane.setAlignment(Pos.BOTTOM_CENTER);
modalPane.usePredefinedTransitionFactories(Side.BOTTOM);
modalPane.show(bottomDialog);
});
// ~
var leftDialog = new Dialog(250, -1);
leftDialog.getChildren().setAll(new Label(
FAKER.country().name()
));
var openLeftBtn = new Button("Left");
openLeftBtn.setOnAction(evt -> {
modalPane.setAlignment(Pos.TOP_LEFT);
modalPane.usePredefinedTransitionFactories(Side.LEFT);
modalPane.show(leftDialog);
});
//snippet_2:end
openTopBtn.setPrefWidth(100);
openRightBtn.setPrefWidth(100);
openBottomBtn.setPrefWidth(100);
openLeftBtn.setPrefWidth(100);
var box = new HBox(HGAP_20, openTopBtn, openRightBtn, openBottomBtn, openLeftBtn);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
The alignment and animated appearance of modal content can be changed \
via corresponding properties.""");
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox persistentExample() {
//snippet_3:start
var dialog = new Dialog(450, 450);
var openBtn = new Button("Open Dialog");
openBtn.setOnAction(evt -> {
modalPane.setPersistent(true);
modalPane.show(dialog);
});
var closeBtn = new Button("Close");
closeBtn.setOnAction(evt -> {
modalPane.hide(true);
modalPane.setPersistent(false);
});
dialog.getChildren().setAll(closeBtn);
//snippet_3:end
var box = new HBox(openBtn);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
By default, the [i]ModalPane[/i] exits when the ESC button is pressed \
or when the mouse is clicked outside the content area. [code]setPersistent()[/code] \
property prevents this behavior and instead plays a bouncing animation.""");
return new ExampleBox(box, new Snippet(getClass(), 3), description);
}
private ExampleBox nestingExample() {
//snippet_4:start
var dialog = new Dialog(600, 600);
var topDialog = new Dialog(450, 450);
var topmostDialog = new Dialog(300, 300);
var openBtn = new Button("Open Dialog 1");
// topViewOrder = -10 (default)
openBtn.setOnAction(evt -> modalPane.show(dialog));
var topDialogBtn = new Button("Open Dialog 2");
topDialogBtn.setOnAction(// topViewOrder = -15
evt -> modalPaneTop.show(topDialog)
);
dialog.getChildren().add(topDialogBtn);
var topmostDialogBtn = new Button("Open Dialog 3");
topmostDialogBtn.setOnAction(// topViewOrder = -20
evt -> modalPaneTopmost.show(topmostDialog)
);
topDialog.getChildren().add(topmostDialogBtn);
//snippet_4:end
var box = new HBox(openBtn);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
There is no a specific support for nested dialogs. But, you can achieve \
the same behavior by stacking multiple modal panes and using the corresponding \
[code]topViewOrder[/code] property value.""");
return new ExampleBox(box, new Snippet(getClass(), 4), description);
}
private ExampleBox maximizedExample() {
//snippet_5:start
var dialog = new Dialog(-1, -1);
var openBtn = new Button("Open Dialog");
openBtn.setOnAction(evt -> modalPane.show(dialog));
var closeBtn = new Button("Close");
closeBtn.setOnAction(evt -> modalPane.hide(true));
dialog.getChildren().setAll(closeBtn);
//snippet_5:end
var box = new HBox(openBtn);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
To create a maximized dialog, simply use a content node such as [i]VBox[/i] \
that expands itself in both the horizontal and vertical directions.""");
return new ExampleBox(box, new Snippet(getClass(), 5), description);
}
private ExampleBox overflowedExample() {
//snippet_6:start
var dialog1 = new Dialog(450, -1);
dialog1.setPadding(new Insets(20));
var openBtn1 = new Button("Open Dialog 1");
openBtn1.setOnAction(evt -> {
StackPane.setMargin(dialog1, new Insets(20));
modalPane.show(dialog1);
});
var textFlow1 = new TextFlow();
dialog1.getChildren().setAll(textFlow1);
for (int i = 0; i < 30; i++) {
textFlow1.getChildren().add(
new Text(FAKER.lorem().paragraph() + "\n\n")
);
}
// ~
var dialog2 = new Dialog(450, -1);
dialog2.setPadding(new Insets(10, 0, 10, 0));
var openBtn2 = new Button("Open Dialog 2");
openBtn2.setOnAction(evt -> {
StackPane.setMargin(dialog2, new Insets(20));
modalPane.show(dialog2);
});
var textFlow2 = new TextFlow();
textFlow2.setMaxWidth(430);
textFlow2.setPadding(new Insets(10, 20, 10, 20));
var scrollPane2 = new ScrollPane(textFlow2);
scrollPane2.setMaxHeight(10_000);
dialog2.getChildren().setAll(scrollPane2);
for (int i = 0; i < 30; i++) {
textFlow2.getChildren().add(
new Text(FAKER.lorem().paragraph() + "\n\n")
);
}
//snippet_6:end
var box = new HBox(HGAP_20, openBtn1, openBtn2);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
The [i]ModalPane[/i] is already scrollable by default, but you can also use a \
[i]ScrollPane[/i] for the content node if needed.""");
return new ExampleBox(box, new Snippet(getClass(), 6), description);
}
private ExampleBox lightboxExample() {
//snippet_7:start
var modalImage = new ImageView();
var thumbnail1 = new ImageView(new Image(
Resources.getResourceAsStream(
"images/gallery/kush-dwivedi-unsplash.jpg"
)));
thumbnail1.setFitWidth(180);
thumbnail1.setFitHeight(120);
thumbnail1.setCursor(Cursor.HAND);
thumbnail1.setOnMouseClicked(evt -> {
modalImage.setImage(thumbnail1.getImage());
modalPane.show(modalImage);
modalImage.requestFocus();
});
var thumbnail2 = new ImageView(new Image(
Resources.getResourceAsStream(
"images/gallery/markus-spiske-unsplash.jpg"
)));
thumbnail2.setFitWidth(180);
thumbnail2.setFitHeight(120);
thumbnail2.setCursor(Cursor.HAND);
thumbnail2.setOnMouseClicked(evt -> {
modalImage.setImage(thumbnail2.getImage());
modalPane.show(modalImage);
modalImage.requestFocus();
});
var thumbnail3 = new ImageView(new Image(
Resources.getResourceAsStream(
"images/gallery/r0m0_4-unsplash.jpg"
)));
thumbnail3.setFitWidth(180);
thumbnail3.setFitHeight(120);
thumbnail3.setCursor(Cursor.HAND);
thumbnail3.setOnMouseClicked(evt -> {
modalImage.setImage(thumbnail3.getImage());
modalPane.show(modalImage);
modalImage.requestFocus();
});
//snippet_7:end
var box = new HBox(5, thumbnail1, thumbnail2, thumbnail3);
box.setAlignment(Pos.CENTER);
var description = BBCodeParser.createFormattedText("""
This simple example demonstrates how [i]ModalPane[/i] can be used to \
implement the famous JS lightbox effect."""
);
return new ExampleBox(box, new Snippet(getClass(), 7), description);
}
///////////////////////////////////////////////////////////////////////////
private static class Dialog extends VBox {
public Dialog(int width, int height) {
super();
setSpacing(10);
setAlignment(Pos.CENTER);
setMinSize(width, height);
setMaxSize(width, height);
setStyle("-fx-background-color: -color-bg-default;");
}
}
}

@ -0,0 +1,155 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.InlineDatePicker;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Popover.ArrowLocation;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.theme.CSSFragment;
import java.time.LocalDate;
import java.time.ZoneId;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
public class PopoverPage extends OutlinePage {
public static final String NAME = "Popover";
@Override
public String getName() {
return NAME;
}
public PopoverPage() {
super();
addFormattedText("""
The [i]Popover[/i] is a control used to display additional information \
or perform actions. It appears as a small popup window that overlays the \
main interface, triggered by a user action such as a mouseover or tap. \
It provides contextual information or options related to a specific object \
or feature on the interface.""");
addSection("Usage", usageExample());
addSection("Position", positionExample());
}
private ExampleBox usageExample() {
var datePickerStyle = """
.popover .date-picker-popup {
-color-date-border: transparent;
-color-date-bg: transparent;
-color-date-day-bg: transparent;
-color-date-month-year-bg: transparent;
-color-date-day-bg-hover: -color-bg-subtle;
}
""";
//snippet_1:start
var textFlow = new TextFlow(new Text(
FAKER.lorem().sentence(30)
));
textFlow.setPrefWidth(300);
textFlow.setPadding(new Insets(10, 0, 10, 0));
var pop1 = new Popover(textFlow);
pop1.setTitle("Lorem Ipsum");
pop1.setHeaderAlwaysVisible(true);
pop1.setDetachable(true);
var link1 = new Hyperlink("Text");
link1.setOnAction(e -> pop1.show(link1));
// ~
var datePicker = new InlineDatePicker();
datePicker.setValue(LocalDate.now(ZoneId.systemDefault()));
// -color-date-border: transparent;
// -color-date-bg: transparent;
// -color-date-day-bg: transparent;
// -color-date-month-year-bg: transparent;
// -color-date-day-bg-hover: -color-bg-subtle;
new CSSFragment(datePickerStyle).addTo(datePicker);
var pop2 = new Popover(datePicker);
pop2.setHeaderAlwaysVisible(false);
pop2.setDetachable(true);
var link2 = new Hyperlink("DatePicker");
link2.setOnAction(e -> pop2.show(link2));
//snippet_1:end
var box = new HBox(HGAP_30, link1, link2);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The popup window has a very lightweight appearance (no default window decorations) \
and an arrow pointing at the owner. Due to the nature of popup windows the \
[i]Popover[/i] will move around with the parent window when the user drags it."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox positionExample() {
//snippet_2:start
class PopoverLink extends Hyperlink {
public PopoverLink(ArrowLocation arrowLocation) {
super();
var textFlow = new TextFlow(new Text(
FAKER.lorem().sentence(30)
));
textFlow.setPrefWidth(300);
textFlow.setPadding(new Insets(10, 0, 10, 0));
var pop = new Popover(textFlow);
pop.setHeaderAlwaysVisible(false);
pop.setArrowLocation(arrowLocation);
setText(String.valueOf(arrowLocation));
setOnAction(e -> pop.show(this));
}
}
var grid = new GridPane();
grid.setHgap(20);
grid.setVgap(20);
grid.addColumn(0,
new PopoverLink(ArrowLocation.TOP_LEFT),
new PopoverLink(ArrowLocation.TOP_CENTER),
new PopoverLink(ArrowLocation.TOP_RIGHT)
);
grid.addColumn(1,
new PopoverLink(ArrowLocation.RIGHT_TOP),
new PopoverLink(ArrowLocation.RIGHT_CENTER),
new PopoverLink(ArrowLocation.RIGHT_BOTTOM)
);
grid.addColumn(2,
new PopoverLink(ArrowLocation.BOTTOM_LEFT),
new PopoverLink(ArrowLocation.BOTTOM_CENTER),
new PopoverLink(ArrowLocation.BOTTOM_RIGHT)
);
grid.addColumn(3,
new PopoverLink(ArrowLocation.LEFT_TOP),
new PopoverLink(ArrowLocation.LEFT_CENTER),
new PopoverLink(ArrowLocation.LEFT_BOTTOM)
);
//snippet_2:end
var description = BBCodeParser.createFormattedText("""
The [i]Popover[/i] popup window can be positioned by setting an \
appropriate ArrowLocation value.."""
);
return new ExampleBox(grid, new Snippet(getClass(), 2), description);
}
}

@ -0,0 +1,93 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.extras;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import javafx.geometry.HorizontalDirection;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
public class ToggleSwitchPage extends OutlinePage {
public static final String NAME = "ToggleSwitch";
@Override
public String getName() {
return NAME;
}
public ToggleSwitchPage() {
super();
addFormattedText("""
The [i]ToggleSwitch[/i] is a control used to activate or deactivate a feature. \
It consists of a horizontal bar with a small knob that can be moved to turn on \
or off a function."""
);
addSection("Usage", usageExample());
addSection("Color", colorExample());
}
private ExampleBox usageExample() {
//snippet_1:start
var toggle1 = new ToggleSwitch("Enabled");
toggle1.selectedProperty().addListener(
(obs, old, val) -> toggle1.setText(val ? "Enabled" : "Disabled")
);
toggle1.setSelected(true);
var toggle2 = new ToggleSwitch("Disabled");
toggle2.selectedProperty().addListener(
(obs, old, val) -> toggle2.setText(val ? "Enabled" : "Disabled")
);
toggle2.setLabelPosition(HorizontalDirection.RIGHT);
toggle2.setSelected(false);
//snippet_1:end
var box = new HBox(HGAP_30, toggle1, toggle2);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
The text position can be changed by setting the [code]labelPosition[/code] \
property value."""
);
return new ExampleBox(box, new Snippet(getClass(), 1), description);
}
private ExampleBox colorExample() {
//snippet_2:start
var successToggle = new ToggleSwitch("Enabled");
successToggle.selectedProperty().addListener((obs, old, val) -> {
successToggle.setText(val ? "Enabled" : "Disabled");
successToggle.pseudoClassStateChanged(Styles.STATE_SUCCESS, val);
}
);
successToggle.setSelected(true);
successToggle.pseudoClassStateChanged(Styles.STATE_SUCCESS, true);
var dangerToggle = new ToggleSwitch("Disabled");
dangerToggle.selectedProperty().addListener((obs, old, val) -> {
dangerToggle.setText(val ? "Enabled" : "Disabled");
dangerToggle.pseudoClassStateChanged(Styles.STATE_DANGER, val);
}
);
dangerToggle.setLabelPosition(HorizontalDirection.RIGHT);
dangerToggle.setSelected(false);
//snippet_2:end
var box = new HBox(HGAP_30, successToggle, dangerToggle);
box.setAlignment(Pos.CENTER_LEFT);
var description = BBCodeParser.createFormattedText("""
Use [code]Styles.STATE_SUCCESS[/code] or [code]Styles.STATE_DANGER[/code] \
pseudo-classes to change the [i]ToggleSwitch[/i] color.""");
return new ExampleBox(box, new Snippet(getClass(), 2), description);
}
}

@ -2,10 +2,6 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import static atlantafx.sampler.util.Controls.hyperlink;
import atlantafx.base.theme.Styles;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -14,105 +10,54 @@ import javafx.animation.KeyFrame;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
class ColorPalette extends VBox { class ColorPalette extends GridPane {
private final List<ColorPaletteBlock> blocks = new ArrayList<>(); private final List<ColorPaletteBlock> blocks = new ArrayList<>();
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE); private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
private final Consumer<ColorPaletteBlock> colorBlockActionHandler; private final Consumer<ColorPaletteBlock> colorBlockActionHandler;
public ColorPalette(Consumer<ColorPaletteBlock> blockClickedHandler) { public ColorPalette(Consumer<ColorPaletteBlock> actionHandler) {
super(); super();
this.colorBlockActionHandler = Objects.requireNonNull(blockClickedHandler); this.colorBlockActionHandler = Objects.requireNonNull(actionHandler, "actionHandler");
createView();
}
private void createView() { add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
var headerLabel = new Label("Color Palette"); add(colorBlock("-color-fg-default", "-color-bg-overlay", "-color-border-default"), 1, 0);
headerLabel.getStyleClass().add(Styles.TITLE_4); add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 2, 0);
add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 3, 0);
var headerBox = new HBox(); add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
headerBox.getChildren().setAll(headerLabel); add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
headerBox.setAlignment(Pos.CENTER_LEFT); add(colorBlock("-color-fg-default", "-color-accent-muted", "-color-accent-emphasis"), 2, 1);
headerBox.getStyleClass().add("header"); add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-emphasis"), 3, 1);
var noteText = new VBox(6); add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2);
noteText.getChildren().setAll( add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
new TextFlow( add(colorBlock("-color-fg-default", "-color-neutral-muted", "-color-neutral-emphasis"), 2, 2);
new Text("Color contrast between text and its background must meet "), add(colorBlock("-color-fg-default", "-color-neutral-subtle", "-color-neutral-emphasis"), 3, 2);
hyperlink(
"required WCAG standards",
URI.create("https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html")
),
new Text(":")
),
new Text(" • 4.5:1 for normal text"),
new Text(" • 3:1 for large text (>24px)"),
new Text(" • 3:1 for UI elements and graphics"),
new Text(" • no contrast requirement for decorative and disabled elements"),
new Text(),
new Text("Click on any color block to observe and modify color combination via built-in contrast checker.")
);
var colorGrid = colorGrid(); add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
add(colorBlock("-color-fg-default", "-color-success-muted", "-color-success-emphasis"), 2, 3);
add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-emphasis"), 3, 3);
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
)); add(colorBlock("-color-fg-default", "-color-warning-muted", "-color-warning-emphasis"), 2, 4);
add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-emphasis"), 3, 4);
add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
add(colorBlock("-color-fg-default", "-color-danger-muted", "-color-danger-emphasis"), 2, 5);
add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-emphasis"), 3, 5);
getChildren().setAll(headerBox, noteText, colorGrid);
setId("color-palette"); setId("color-palette");
} }
private GridPane colorGrid() {
var grid = new GridPane();
grid.getStyleClass().add("grid");
grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
grid.add(colorBlock("-color-fg-default", "-color-bg-overlay", "-color-border-default"), 1, 0);
grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 2, 0);
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 3, 0);
grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
grid.add(colorBlock("-color-fg-default", "-color-accent-muted", "-color-accent-emphasis"), 2, 1);
grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-emphasis"), 3, 1);
grid.add(
colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2
);
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
grid.add(colorBlock("-color-fg-default", "-color-neutral-muted", "-color-neutral-emphasis"), 2, 2);
grid.add(colorBlock("-color-fg-default", "-color-neutral-subtle", "-color-neutral-emphasis"), 3, 2);
grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
grid.add(colorBlock("-color-fg-default", "-color-success-muted", "-color-success-emphasis"), 2, 3);
grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-emphasis"), 3, 3);
grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
grid.add(colorBlock("-color-fg-default", "-color-warning-muted", "-color-warning-emphasis"), 2, 4);
grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-emphasis"), 3, 4);
grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
grid.add(colorBlock("-color-fg-default", "-color-danger-muted", "-color-danger-emphasis"), 2, 5);
grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-emphasis"), 3, 5);
return grid;
}
private ColorPaletteBlock colorBlock(String fgColor, String bgColor, String borderColor) { private ColorPaletteBlock colorBlock(String fgColor, String bgColor, String borderColor) {
var block = new ColorPaletteBlock(fgColor, bgColor, borderColor, bgBaseColor.getReadOnlyProperty()); var block = new ColorPaletteBlock(fgColor, bgColor, borderColor, bgBaseColor.getReadOnlyProperty());
block.setOnAction(colorBlockActionHandler); block.setOnAction(colorBlockActionHandler);

@ -2,23 +2,16 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import atlantafx.base.theme.Styles;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
class ColorScale extends VBox { class ColorScale extends FlowPane {
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE); private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
private final List<ColorScaleBlock> blocks = Arrays.asList( private final List<ColorScaleBlock> blocks = Arrays.asList(
@ -32,35 +25,15 @@ class ColorScale extends VBox {
public ColorScale() { public ColorScale() {
super(); super();
createView();
}
private void createView() {
var headerLabel = new Label("Color Scale");
headerLabel.getStyleClass().add(Styles.TITLE_4);
var headerBox = new HBox();
headerBox.getChildren().setAll(headerLabel);
headerBox.setAlignment(Pos.CENTER_LEFT);
headerBox.getStyleClass().add("header");
var noteText = new TextFlow(
new Text(
"Avoid referencing scale variables directly when building UI that needs "
+ "to adapt to different color themes. Instead, use the functional variables listed above."
)
);
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set(
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE val != null && !val.getFills().isEmpty()
? (Color) val.getFills().get(0).getFill()
: Color.WHITE
)); ));
setId("color-scale"); setId("color-scale");
getChildren().setAll( getChildren().setAll(blocks);
headerBox,
noteText,
colorTable()
);
} }
public void updateColorInfo(Duration delay) { public void updateColorInfo(Duration delay) {
@ -68,11 +41,4 @@ class ColorScale extends VBox {
t.setOnFinished(e -> blocks.forEach(ColorScaleBlock::update)); t.setOnFinished(e -> blocks.forEach(ColorScaleBlock::update));
t.play(); t.play();
} }
private FlowPane colorTable() {
var root = new FlowPane(20, 20);
root.getStyleClass().add("table");
root.getChildren().setAll(blocks);
return root;
}
} }

@ -14,7 +14,7 @@ import javafx.scene.paint.Color;
class ColorScaleBlock extends VBox { class ColorScaleBlock extends VBox {
private static final double BLOCK_WIDTH = 250; private static final double BLOCK_WIDTH = 200;
private static final double BLOCK_HEIGHT = 40; private static final double BLOCK_HEIGHT = 40;
private final ReadOnlyObjectProperty<Color> bgBaseColor; private final ReadOnlyObjectProperty<Color> bgBaseColor;

@ -2,25 +2,20 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static atlantafx.sampler.util.Controls.hyperlink;
import atlantafx.base.controls.CustomTextField; import atlantafx.base.controls.CustomTextField;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Snippet;
import atlantafx.sampler.theme.CSSFragment; import atlantafx.sampler.theme.CSSFragment;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.Node;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.javafx.StackedFontIcon; import org.kordamp.ikonli.javafx.StackedFontIcon;
@ -28,7 +23,7 @@ import org.kordamp.ikonli.material2.Material2AL;
import org.kordamp.ikonli.material2.Material2MZ; import org.kordamp.ikonli.material2.Material2MZ;
import org.kordamp.ikonli.material2.Material2OutlinedAL; import org.kordamp.ikonli.material2.Material2OutlinedAL;
public class IconsPage extends AbstractPage { public class IconsPage extends OutlinePage {
public static final String NAME = "Icons"; public static final String NAME = "Icons";
@ -39,21 +34,116 @@ public class IconsPage extends AbstractPage {
public IconsPage() { public IconsPage() {
super(); super();
createView();
addFormattedText("""
AtlantaFX supports [url=https://kordamp.org/ikonli]Ikonli[/url] iconic fonts out \
of the box, which can be used in conjunction with certain JavaFX components.""", true);
addSection("Color", colorExample());
addSection("Stacking", stackingExample());
addSection("Icon Pack", iconBrowser());
} }
private void createView() { private ExampleBox colorExample() {
var headerText = new TextFlow( //snippet_1:start
new Text("AtlantaFX supports "), var accentIcon = new FontIcon(Material2MZ.THUMB_UP);
hyperlink("Ikonli", URI.create("https://kordamp.org/ikonli")), accentIcon.getStyleClass().add(Styles.ACCENT);
new Text(" iconic fonts that can be used together with some JavaFX components.")
);
var browserText = new TextFlow( var successIcon = new FontIcon(Material2MZ.THUMB_UP);
new Text("There's a variety of icon packs. Sampler app uses "), successIcon.getStyleClass().add(Styles.SUCCESS);
hyperlink("Material Icons", URI.create("https://kordamp.org/ikonli/cheat-sheet-material2.html")),
new Text(" you can preview below.") var warningIcon = new FontIcon(Material2MZ.THUMB_UP);
warningIcon.getStyleClass().add(Styles.WARNING);
var dangerIcon = new FontIcon(Material2MZ.THUMB_UP);
dangerIcon.getStyleClass().add(Styles.DANGER);
//snippet_1:end
var box = new HBox(HGAP_20, accentIcon, successIcon, warningIcon, dangerIcon);
var description = BBCodeParser.createFormattedText("""
You can use pseudo-classes to set the icon color.""");
var example = new ExampleBox(box, new Snippet(getClass(), 1), description);
example.setAllowDisable(false);
return example;
}
private ExampleBox stackingExample() {
var style1 = """
.stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 48px;
-fx-icon-color: -color-danger-emphasis;
}
.stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 24px;
}
""";
var style2 = """
.stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 48px;
}
.stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 24px;
}
""";
//snippet_2:start
var outerIcon1 = new FontIcon(Material2OutlinedAL.BLOCK);
outerIcon1.getStyleClass().add("outer-icon");
var innerIcon1 = new FontIcon(Material2MZ.PHOTO_CAMERA);
innerIcon1.getStyleClass().add("inner-icon");
var stackIcon1 = new StackedFontIcon();
stackIcon1.getChildren().addAll(innerIcon1, outerIcon1);
// .stacked-ikonli-font-icon > .outer-icon {
// -fx-icon-size: 48px;
// -fx-icon-color: -color-danger-emphasis;
// }
// .stacked-ikonli-font-icon > .inner-icon {
// -fx-icon-size: 24px;
// }
new CSSFragment(style1).addTo(stackIcon1);
var outerIcon2 = new FontIcon(
Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK
); );
outerIcon2.getStyleClass().add("outer-icon");
var innerIcon2 = new FontIcon(Material2AL.LOCK);
innerIcon2.getStyleClass().add("inner-icon");
var stackIcon2 = new StackedFontIcon();
stackIcon2.getChildren().addAll(outerIcon2, innerIcon2);
// .stacked-ikonli-font-icon > .outer-icon {
// -fx-icon-size: 48px;
// }
// .stacked-ikonli-font-icon > .inner-icon {
// -fx-icon-size: 24px;
// }
new CSSFragment(style2).addTo(stackIcon2);
//snippet_2:end
var box = new HBox(HGAP_20, stackIcon1, stackIcon2);
var description = BBCodeParser.createFormattedText("""
Ikonli also supports icon stacking, although it is currently in an \
initial state and requires some manual styling. However, it can be \
useful in certain cases.""");
var example = new ExampleBox(box, new Snippet(getClass(), 2), description);
example.setAllowDisable(false);
return example;
}
private Node iconBrowser() {
var description = createFormattedText("""
There are various icon packs available. The Sampler app uses \
[url=https://kordamp.org/ikonli/cheat-sheet-material2.html]Material Icons[/url] \
which you can preview below.""", true);
var filterText = new CustomTextField(); var filterText = new CustomTextField();
filterText.setLeft(new FontIcon(Material2MZ.SEARCH)); filterText.setLeft(new FontIcon(Material2MZ.SEARCH));
@ -69,77 +159,8 @@ public class IconsPage extends AbstractPage {
var iconBrowser = new IconBrowser(5, icons); var iconBrowser = new IconBrowser(5, icons);
VBox.setVgrow(iconBrowser, Priority.ALWAYS); VBox.setVgrow(iconBrowser, Priority.ALWAYS);
iconBrowser.filterProperty().bind(filterText.textProperty()); iconBrowser.filterProperty().bind(filterText.textProperty());
iconBrowser.setMinHeight(500);
setUserContent(new VBox( return new VBox(VGAP_10, description, filterBox, iconBrowser);
PAGE_VGAP,
headerText,
expandingHBox(colorSample(), stackingSample()),
browserText,
filterBox,
iconBrowser
));
}
private SampleBlock colorSample() {
var accentIcon = new FontIcon(Material2MZ.THUMB_UP);
accentIcon.getStyleClass().add(Styles.ACCENT);
var successIcon = new FontIcon(Material2MZ.THUMB_UP);
successIcon.getStyleClass().add(Styles.SUCCESS);
var warningIcon = new FontIcon(Material2MZ.THUMB_UP);
warningIcon.getStyleClass().add(Styles.WARNING);
var dangerIcon = new FontIcon(Material2MZ.THUMB_UP);
dangerIcon.getStyleClass().add(Styles.DANGER);
var content = new VBox(
BLOCK_VGAP,
new Label("You can also use pseudo-classes to set icon color."),
new HBox(BLOCK_HGAP, accentIcon, successIcon, warningIcon, dangerIcon)
);
return new SampleBlock("Colors", content);
}
private SampleBlock stackingSample() {
var outerIcon1 = new FontIcon(Material2OutlinedAL.BLOCK);
outerIcon1.getStyleClass().add("outer-icon");
var innerIcon1 = new FontIcon(Material2MZ.PHOTO_CAMERA);
innerIcon1.getStyleClass().add("inner-icon");
var stackIcon1 = new StackedFontIcon();
stackIcon1.getChildren().addAll(innerIcon1, outerIcon1);
new CSSFragment("""
.stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 48px;
-fx-icon-color: -color-danger-emphasis;
}
.stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 24px;
}
""").addTo(stackIcon1);
var outerIcon2 = new FontIcon(Material2OutlinedAL.CHECK_BOX_OUTLINE_BLANK);
outerIcon2.getStyleClass().add("outer-icon");
var innerIcon2 = new FontIcon(Material2AL.LOCK);
innerIcon2.getStyleClass().add("inner-icon");
var stackIcon2 = new StackedFontIcon();
stackIcon2.getChildren().addAll(outerIcon2, innerIcon2);
new CSSFragment("""
.stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 48px;
}
.stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 24px;
}
""").addTo(stackIcon2);
var content = new HBox(BLOCK_HGAP, stackIcon1, stackIcon2);
return new SampleBlock("Stacking Icons", content);
} }
} }

@ -0,0 +1,71 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.general;
import static atlantafx.sampler.util.Containers.setScrollConstraints;
import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED;
import atlantafx.sampler.Resources;
import atlantafx.sampler.page.Page;
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class OverviewPage extends ScrollPane implements Page {
public static final String NAME = "Overview";
@Override
public String getName() {
return NAME;
}
public OverviewPage() {
super();
try {
var wrapper = new VBox();
wrapper.setAlignment(Pos.TOP_CENTER);
var loader = new FXMLLoader(
Resources.getResource("assets/fxml/overview/index.fxml").toURL()
);
Parent fxmlContent = loader.load();
((Pane) fxmlContent).setMaxWidth(Page.MAX_WIDTH);
VBox.setVgrow(fxmlContent, Priority.ALWAYS);
wrapper.getChildren().setAll(fxmlContent);
setScrollConstraints(this, AS_NEEDED, true, AS_NEEDED, true);
setMaxHeight(20_000);
setContent(wrapper);
} catch (IOException e) {
throw new RuntimeException("Unable to load FXML file", e);
}
setId("overview");
}
@Override
public Parent getView() {
return this;
}
@Override
public boolean canDisplaySourceCode() {
return true;
}
@Override
public boolean canChangeThemeSettings() {
return true;
}
@Override
public void reset() {
}
}

@ -2,71 +2,51 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import static atlantafx.sampler.event.ThemeEvent.EventType.COLOR_CHANGE; import static atlantafx.sampler.event.ThemeEvent.EventType;
import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_ADD;
import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_CHANGE;
import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_REMOVE;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static atlantafx.sampler.util.Controls.hyperlink;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.event.ThemeEvent; import atlantafx.sampler.event.ThemeEvent;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.theme.SamplerTheme; import atlantafx.sampler.theme.SamplerTheme;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import java.net.URI; import atlantafx.sampler.util.Lazy;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.collections.FXCollections;
import javafx.geometry.HPos; import javafx.geometry.HPos;
import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.paint.Color;
import javafx.scene.text.TextFlow; import javafx.scene.text.Font;
import javafx.util.Duration; import javafx.util.Duration;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2OutlinedMZ; import org.kordamp.ikonli.material2.Material2OutlinedMZ;
@SuppressWarnings("UnnecessaryLambda") @SuppressWarnings("UnnecessaryLambda")
public class ThemePage extends AbstractPage { public class ThemePage extends OutlinePage {
public static final String NAME = "Theme"; public static final String NAME = "Theme";
private static final ThemeManager TM = ThemeManager.getInstance(); private static final ThemeManager TM = ThemeManager.getInstance();
private static final String DEFAULT_FONT_ID = "Default";
private static final Image SCENE_BUILDER_ICON = new Image( private static final Image SCENE_BUILDER_ICON = new Image(
Resources.getResourceAsStream("images/scene-builder_32.png") Resources.getResourceAsStream("images/scene-builder_32.png")
); );
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
dialog.getContent().setValues(
colorBlock.getFgColorName(),
colorBlock.getFgColor(),
colorBlock.getBgColorName(),
colorBlock.getBgColor()
);
overlay.setContent(dialog, HPos.CENTER);
overlay.toFront();
};
private final ColorPalette colorPalette = new ColorPalette(colorBlockActionHandler);
private final ColorScale colorScale = new ColorScale();
private final ChoiceBox<SamplerTheme> themeSelector = createThemeSelector();
private ThemeRepoManagerDialog themeRepoManagerDialog;
private ContrastCheckerDialog contrastCheckerDialog;
private SceneBuilderDialog sceneBuilderDialog;
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;
@ -82,21 +62,81 @@ public class ThemePage extends AbstractPage {
return false; return false;
} }
///////////////////////////////////////////////////////////////////////////
private final Lazy<ThemeRepoManagerDialog> themeRepoManagerDialog;
private final Lazy<ContrastCheckerDialog> contrastCheckerDialog;
private final Lazy<SceneBuilderDialog> sceneBuilderDialog;
private final ColorPalette colorPalette;
private final ColorScale colorScale = new ColorScale();
private final ChoiceBox<SamplerTheme> themeSelector = createThemeSelector();
private final ComboBox<String> fontFamilyChooser = createFontFamilyChooser();
private final Spinner<Integer> fontSizeSpinner = createFontSizeSpinner();
public ThemePage() { public ThemePage() {
super(); super();
createView(); colorPalette = new ColorPalette(colorBlock -> {
ContrastCheckerDialog dialog = getContrastCheckerDialog();
dialog.getContent().setValues(
colorBlock.getFgColorName(),
colorBlock.getFgColor(),
colorBlock.getBgColorName(),
colorBlock.getBgColor()
);
overlay.setContent(dialog, HPos.CENTER);
overlay.toFront();
});
themeRepoManagerDialog = new Lazy<>(() -> {
var dialog = new ThemeRepoManagerDialog();
dialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
});
return dialog;
});
contrastCheckerDialog = new Lazy<>(() -> {
var dialog = new ContrastCheckerDialog(getBgBaseColorProperty());
dialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
});
return dialog;
});
sceneBuilderDialog = new Lazy<>(() -> {
var dialog = new SceneBuilderDialog();
dialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
dialog.reset();
});
return dialog;
});
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> { DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
if (e.getEventType() == THEME_ADD || e.getEventType() == THEME_REMOVE) { var eventType = e.getEventType();
if (eventType == EventType.THEME_ADD || eventType == EventType.THEME_REMOVE) {
themeSelector.getItems().setAll(TM.getRepository().getAll()); themeSelector.getItems().setAll(TM.getRepository().getAll());
selectCurrentTheme(); selectCurrentTheme();
} }
if (e.getEventType() == THEME_CHANGE || e.getEventType() == COLOR_CHANGE) { if (eventType == EventType.THEME_CHANGE || eventType == EventType.COLOR_CHANGE) {
colorPalette.updateColorInfo(Duration.seconds(1)); colorPalette.updateColorInfo(Duration.seconds(1));
colorScale.updateColorInfo(Duration.seconds(1)); colorScale.updateColorInfo(Duration.seconds(1));
} }
}); });
addSection("Theme", createThemeManagementSection());
addSection("Scene Builder", createSceneBuilderSection());
addSection("Color Palette", createColorPaletteSection());
addSection("Color Scale", createColorScaleSection());
selectCurrentTheme();
} }
@Override @Override
@ -106,31 +146,12 @@ public class ThemePage extends AbstractPage {
colorScale.updateColorInfo(Duration.ZERO); colorScale.updateColorInfo(Duration.ZERO);
} }
private void createView() { private Node createThemeManagementSection() {
var noteText = new TextFlow(
new Text("AtlantaFX follows "),
hyperlink("Github Primer interface guidelines",
URI.create("https://primer.style/design/foundations/color")),
new Text(" and color system.")
);
setUserContent(new VBox(
Page.PAGE_VGAP,
createOptionsGrid(),
noteText,
colorPalette,
colorScale
));
selectCurrentTheme();
}
private GridPane createOptionsGrid() {
var themeRepoBtn = new Button("", new FontIcon(Material2OutlinedMZ.SETTINGS)); var themeRepoBtn = new Button("", new FontIcon(Material2OutlinedMZ.SETTINGS));
themeRepoBtn.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT); themeRepoBtn.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT);
themeRepoBtn.setTooltip(new Tooltip("Settings")); themeRepoBtn.setTooltip(new Tooltip("Settings"));
themeRepoBtn.setOnAction(e -> { themeRepoBtn.setOnAction(e -> {
ThemeRepoManagerDialog dialog = getOrCreateThemeRepoManagerDialog(); ThemeRepoManagerDialog dialog = themeRepoManagerDialog.get();
overlay.setContent(dialog, HPos.CENTER); overlay.setContent(dialog, HPos.CENTER);
dialog.getContent().update(); dialog.getContent().update();
overlay.toFront(); overlay.toFront();
@ -138,41 +159,78 @@ public class ThemePage extends AbstractPage {
var accentSelector = new AccentColorSelector(); var accentSelector = new AccentColorSelector();
var sceneBuilderBtn = new Button("SceneBuilder Integration");
sceneBuilderBtn.setGraphic(new ImageView(SCENE_BUILDER_ICON));
sceneBuilderBtn.setOnAction(e -> {
SceneBuilderDialog dialog = getOrCreateScneBuilderDialog();
overlay.setContent(dialog, HPos.CENTER);
overlay.toFront();
});
// ~ // ~
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(BLOCK_HGAP); grid.setHgap(HGAP_20);
grid.setVgap(BLOCK_VGAP); grid.setVgap(VGAP_10);
grid.addRow(0, new Label("Color theme"), themeSelector, themeRepoBtn);
grid.add(new Label("Color theme"), 0, 0); grid.addRow(1, new Label("Accent color"), accentSelector);
grid.add(themeSelector, 1, 0); grid.addRow(2, new Label("Font"), new HBox(10, fontFamilyChooser, fontSizeSpinner));
grid.add(themeRepoBtn, 2, 0);
grid.add(new Label("Accent color"), 0, 1);
grid.add(accentSelector, 1, 1);
grid.add(sceneBuilderBtn, 0, 2, GridPane.REMAINING, 1);
return grid; return grid;
} }
private Node createSceneBuilderSection() {
var sceneBuilderBtn = new Button("SceneBuilder Integration");
sceneBuilderBtn.setGraphic(new ImageView(SCENE_BUILDER_ICON));
sceneBuilderBtn.setOnAction(e -> {
SceneBuilderDialog dialog = sceneBuilderDialog.get();
overlay.setContent(dialog, HPos.CENTER);
overlay.toFront();
});
var description = BBCodeParser.createFormattedText("""
While SceneBuilder does not support adding custom themes, it is \
possible to overwrite looked-up CSS paths to make the existing \
SceneBuilder menu options load custom CSS files."""
);
return new VBox(VGAP_20, description, sceneBuilderBtn);
}
private Node createColorPaletteSection() {
var description = createFormattedText("""
AtlantaFX follows [url=https://primer.style/design/foundations/color]GitHub \
Primer interface guidelines[/url] and color system.
Color contrast between text and its background must meet \
[url=https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html]required WCAG standards[/url]:
[ul]
[li]4.5:1 for normal text[/li]
[li]3:1 for large text (>24px)[/li]
[li]3:1 for UI elements and graphics[/li]
[li]no contrast requirement for decorative and disabled elements[/li][/ul]
Click on any color block to observe and modify color combination via built-in contrast checker.
""", true
);
return new VBox(VGAP_10, description, colorPalette);
}
private Node createColorScaleSection() {
var description = createFormattedText("""
Avoid referencing scale variables directly when building UI that needs \
to adapt to different color themes. Instead, use the functional variables \
listed above.""", false
);
return new VBox(VGAP_10, description, colorScale);
}
private ChoiceBox<SamplerTheme> createThemeSelector() { private ChoiceBox<SamplerTheme> createThemeSelector() {
var selector = new ChoiceBox<SamplerTheme>(); var choiceBox = new ChoiceBox<SamplerTheme>();
selector.getItems().setAll(TM.getRepository().getAll()); choiceBox.getItems().setAll(TM.getRepository().getAll());
selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> { choiceBox.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) { if (val != null && getScene() != null) {
TM.setTheme(val); TM.setTheme(val);
} }
}); });
selector.setPrefWidth(250); choiceBox.setPrefWidth(310);
selector.setConverter(new StringConverter<>() { choiceBox.setConverter(new StringConverter<>() {
@Override @Override
public String toString(SamplerTheme theme) { public String toString(SamplerTheme theme) {
return theme != null ? theme.getName() : ""; return theme != null ? theme.getName() : "";
@ -187,56 +245,66 @@ public class ThemePage extends AbstractPage {
} }
}); });
return selector; return choiceBox;
}
private ComboBox<String> createFontFamilyChooser() {
var comboBox = new ComboBox<String>();
comboBox.setPrefWidth(200);
// keyword to reset font family to its default value
comboBox.getItems().add(DEFAULT_FONT_ID);
comboBox.getItems().addAll(FXCollections.observableArrayList(Font.getFamilies()));
// select active font family value on page load
comboBox.getSelectionModel().select(TM.getFontFamily());
comboBox.valueProperty().addListener((obs, old, val) -> {
if (val != null) {
TM.setFontFamily(DEFAULT_FONT_ID.equals(val) ? ThemeManager.DEFAULT_FONT_FAMILY_NAME : val);
}
});
return comboBox;
}
private Spinner<Integer> createFontSizeSpinner() {
var spinner = new Spinner<Integer>(
ThemeManager.SUPPORTED_FONT_SIZE.get(0),
ThemeManager.SUPPORTED_FONT_SIZE.get(ThemeManager.SUPPORTED_FONT_SIZE.size() - 1),
TM.getFontSize()
);
spinner.setPrefWidth(100);
// Instead of this we should obtain font size from a rendered node.
// But since it's not trivial (thanks to JavaFX doesn't expose relevant API)
// we just keep current font size inside ThemeManager singleton.
// It works fine if ThemeManager default font size value matches
// default theme font size value.
spinner.getValueFactory().setValue(TM.getFontSize());
spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null) {
TM.setFontSize(val);
}
});
return spinner;
} }
private void selectCurrentTheme() { private void selectCurrentTheme() {
if (TM.getTheme() == null) { if (TM.getTheme() != null) {
return; themeSelector.getItems().stream()
.filter(t -> Objects.equals(TM.getTheme().getName(), t.getName()))
.findFirst()
.ifPresent(t -> themeSelector.getSelectionModel().select(t));
} }
themeSelector.getItems().stream()
.filter(t -> Objects.equals(TM.getTheme().getName(), t.getName()))
.findFirst()
.ifPresent(t -> themeSelector.getSelectionModel().select(t));
} }
private ThemeRepoManagerDialog getOrCreateThemeRepoManagerDialog() { private ContrastCheckerDialog getContrastCheckerDialog() {
if (themeRepoManagerDialog == null) { return contrastCheckerDialog.get();
themeRepoManagerDialog = new ThemeRepoManagerDialog();
}
themeRepoManagerDialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
});
return themeRepoManagerDialog;
} }
private ContrastCheckerDialog getOrCreateContrastCheckerDialog() { private ReadOnlyObjectProperty<Color> getBgBaseColorProperty() {
if (contrastCheckerDialog == null) { return colorPalette.bgBaseColorProperty();
contrastCheckerDialog = new ContrastCheckerDialog(colorPalette.bgBaseColorProperty());
}
contrastCheckerDialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
});
return contrastCheckerDialog;
}
private SceneBuilderDialog getOrCreateScneBuilderDialog() {
if (sceneBuilderDialog == null) {
sceneBuilderDialog = new SceneBuilderDialog();
}
sceneBuilderDialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
sceneBuilderDialog.reset();
});
return sceneBuilderDialog;
} }
} }

@ -2,360 +2,323 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import static atlantafx.base.theme.Styles.ACCENT; import static atlantafx.sampler.event.ThemeEvent.EventType;
import static atlantafx.base.theme.Styles.DANGER;
import static atlantafx.base.theme.Styles.SUCCESS;
import static atlantafx.base.theme.Styles.TEXT;
import static atlantafx.base.theme.Styles.TEXT_BOLD;
import static atlantafx.base.theme.Styles.TEXT_BOLDER;
import static atlantafx.base.theme.Styles.TEXT_CAPTION;
import static atlantafx.base.theme.Styles.TEXT_ITALIC;
import static atlantafx.base.theme.Styles.TEXT_LIGHTER;
import static atlantafx.base.theme.Styles.TEXT_MUTED;
import static atlantafx.base.theme.Styles.TEXT_NORMAL;
import static atlantafx.base.theme.Styles.TEXT_OBLIQUE;
import static atlantafx.base.theme.Styles.TEXT_SMALL;
import static atlantafx.base.theme.Styles.TEXT_STRIKETHROUGH;
import static atlantafx.base.theme.Styles.TEXT_SUBTLE;
import static atlantafx.base.theme.Styles.TEXT_UNDERLINED;
import static atlantafx.base.theme.Styles.TITLE_1;
import static atlantafx.base.theme.Styles.TITLE_2;
import static atlantafx.base.theme.Styles.TITLE_3;
import static atlantafx.base.theme.Styles.TITLE_4;
import static atlantafx.base.theme.Styles.WARNING;
import static atlantafx.sampler.event.ThemeEvent.EventType.FONT_CHANGE;
import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_CHANGE;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import static atlantafx.sampler.theme.ThemeManager.DEFAULT_FONT_FAMILY_NAME;
import static atlantafx.sampler.theme.ThemeManager.SUPPORTED_FONT_SIZE;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.BBCodeParser;
import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.DefaultEventBus;
import atlantafx.sampler.event.ThemeEvent; import atlantafx.sampler.event.ThemeEvent;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.ExampleBox;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.OutlinePage;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.page.Snippet;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.collections.FXCollections;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration; import javafx.util.Duration;
public class TypographyPage extends AbstractPage { public class TypographyPage extends OutlinePage {
public static final String NAME = "Typography"; public static final String NAME = "Typography";
private static final int CONTROL_WIDTH = 200;
private static final String DEFAULT_FONT_ID = "Default";
private static final ThemeManager TM = ThemeManager.getInstance();
private Pane fontSizeSampleContent; private GridPane fontSizeGridPane;
@Override @Override
public String getName() { public String getName() {
return NAME; return NAME;
} }
@Override
public boolean canDisplaySourceCode() {
return false;
}
@Override
public boolean canChangeThemeSettings() {
return false;
}
public TypographyPage() { public TypographyPage() {
super(); super();
createView(); addFormattedText("""
Because AtlantaFX is also distributed as a single CSS file, it does not come \
with any fonts. However, it does support several utility classes demonstrated \
below that can be used to manipulate font properties. If you need a formatted \
text support have a look at [i]BBCodeParser[/i].""");
addSection("Font Size", fontSizeExample());
addSection("Font Weight", fontWeightExample());
addSection("Font Style", fontStyleExample());
addSection("Text Color", textColorExample());
addSection("Hyperlink", hyperlinkExample());
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> { DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
if (e.getEventType() == THEME_CHANGE || e.getEventType() == FONT_CHANGE) { var eventType = e.getEventType();
if (eventType == EventType.THEME_CHANGE || eventType == EventType.FONT_CHANGE) {
updateFontInfo(Duration.seconds(1)); updateFontInfo(Duration.seconds(1));
} }
}); });
} }
private void createView() {
var controlsGrid = new GridPane();
controlsGrid.setHgap(BLOCK_HGAP);
controlsGrid.setVgap(BLOCK_VGAP);
controlsGrid.add(new Label("Font family"), 0, 0);
controlsGrid.add(createFontFamilyChooser(), 1, 0);
controlsGrid.add(new Label("Font size"), 0, 1);
controlsGrid.add(crateFontSizeSpinner(), 1, 1);
var fontSizeSample = fontSizeSample();
fontSizeSampleContent = (Pane) fontSizeSample.getContent();
setUserContent(new VBox(
PAGE_VGAP,
controlsGrid,
fontSizeSample,
fontWeightSample(),
expandingHBox(fontStyleSample(), textColorSample(), hyperlinkSample()),
textFlowSample()
));
}
@Override @Override
protected void onRendered() { protected void onRendered() {
super.onRendered(); super.onRendered();
// font metrics can only be obtained by requesting from a rendered node // font metrics can only be obtained by requesting from a rendered node
updateFontInfo(Duration.ZERO); updateFontInfo(Duration.seconds(1));
}
private ComboBox<String> createFontFamilyChooser() {
ComboBox<String> comboBox = new ComboBox<>();
comboBox.getItems().add(DEFAULT_FONT_ID); // keyword to reset font family to its default value
comboBox.getItems().addAll(FXCollections.observableArrayList(Font.getFamilies()));
comboBox.setPrefWidth(CONTROL_WIDTH);
comboBox.getSelectionModel().select(TM.getFontFamily()); // select active font family value on page load
comboBox.valueProperty().addListener((obs, old, val) -> {
if (val != null) {
TM.setFontFamily(DEFAULT_FONT_ID.equals(val) ? DEFAULT_FONT_FAMILY_NAME : val);
}
});
return comboBox;
}
private Spinner<Integer> crateFontSizeSpinner() {
var spinner = new Spinner<Integer>(
SUPPORTED_FONT_SIZE.get(0),
SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1),
TM.getFontSize()
);
spinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
spinner.setPrefWidth(CONTROL_WIDTH);
// Instead of this we should obtain font size from a rendered node.
// But since it's not trivial (thanks to JavaFX doesn't expose relevant API)
// we just keep current font size inside ThemeManager singleton.
// It works fine if ThemeManager default font size value matches
// default theme font size value.
spinner.getValueFactory().setValue(TM.getFontSize());
spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null) {
TM.setFontSize(val);
updateFontInfo(Duration.seconds(1));
}
});
return spinner;
} }
private void updateFontInfo(Duration delay) { private void updateFontInfo(Duration delay) {
var t = new Timeline(new KeyFrame(delay)); if (fontSizeGridPane == null) {
t.setOnFinished(e -> { return;
Map<String, Node> map = fontSizeSampleContent.getChildren().stream() }
.collect(Collectors.toMap(
n -> GridPane.getColumnIndex(n).toString() + GridPane.getRowIndex(n).toString(), Map<String, Node> map = fontSizeGridPane.getChildren().stream()
n -> n .collect(Collectors.toMap(
)); n -> GridPane.getColumnIndex(n).toString() + GridPane.getRowIndex(n).toString(),
((Label) map.get("10")).setText(String.format("%.0fpx", getFontSize(map.get("00")))); n -> n
((Label) map.get("11")).setText(String.format("%.0fpx", getFontSize(map.get("01")))); ));
((Label) map.get("12")).setText(String.format("%.0fpx", getFontSize(map.get("02")))); ((Label) map.get("10")).setText(String.format("=%.0fpx", getFontSize(map.get("00"))));
((Label) map.get("13")).setText(String.format("%.0fpx", getFontSize(map.get("03")))); ((Label) map.get("11")).setText(String.format("=%.0fpx", getFontSize(map.get("01"))));
((Label) map.get("30")).setText(String.format("%.0fpx", getFontSize(map.get("20")))); ((Label) map.get("12")).setText(String.format("=%.0fpx", getFontSize(map.get("02"))));
((Label) map.get("31")).setText(String.format("%.0fpx", getFontSize(map.get("21")))); ((Label) map.get("13")).setText(String.format("=%.0fpx", getFontSize(map.get("03"))));
((Label) map.get("32")).setText(String.format("%.0fpx", getFontSize(map.get("22")))); ((Label) map.get("30")).setText(String.format("=%.0fpx", getFontSize(map.get("20"))));
}); ((Label) map.get("31")).setText(String.format("=%.0fpx", getFontSize(map.get("21"))));
t.play(); ((Label) map.get("32")).setText(String.format("=%.0fpx", getFontSize(map.get("22"))));
} }
private double getFontSize(Node node) { private double getFontSize(Node node) {
return (node instanceof Text text) ? Math.ceil(text.getFont().getSize()) : 0; return (node instanceof Text text) ? Math.ceil(text.getFont().getSize()) : 0;
} }
private SampleBlock fontSizeSample() { ///////////////////////////////////////////////////////////////////////////
private ExampleBox fontSizeExample() {
//snippet_1:start
var title1Text = new Text("Title 1");
title1Text.getStyleClass().addAll(Styles.TITLE_1);
var title2Text = new Text("Title 2");
title2Text.getStyleClass().addAll(Styles.TITLE_2);
var title3Text = new Text("Title 3");
title3Text.getStyleClass().addAll(Styles.TITLE_3);
var title4Text = new Text("Title 4");
title4Text.getStyleClass().addAll(Styles.TITLE_4);
var captionText = new Text("Caption");
captionText.getStyleClass().addAll(Styles.TEXT_CAPTION);
var defaultText = new Text("Default");
var smallText = new Text("Small");
smallText.getStyleClass().addAll(Styles.TEXT_SMALL);
//snippet_1:end
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(BLOCK_HGAP); grid.setHgap(HGAP_20);
grid.setVgap(BLOCK_VGAP); grid.setVgap(VGAP_20);
grid.addRow(0,
grid.add(createText("Title 1", TITLE_1), 0, 0); title1Text, createFontSizeLabel(),
grid.add(createFontSizeLabel(), 1, 0); captionText, createFontSizeLabel()
grid.add(createText("Title 2", TITLE_2), 0, 1); );
grid.add(createFontSizeLabel(), 1, 1); grid.addRow(1,
grid.add(createText("Title 3", TITLE_3), 0, 2); title2Text, createFontSizeLabel(),
grid.add(createFontSizeLabel(), 1, 2); defaultText, createFontSizeLabel()
grid.add(createText("Title 4", TITLE_4), 0, 3); );
grid.add(createFontSizeLabel(), 1, 3); grid.addRow(2,
title3Text, createFontSizeLabel(),
grid.add(createText("Caption", TEXT_CAPTION), 2, 0); smallText, createFontSizeLabel()
grid.add(createFontSizeLabel(), 3, 0); );
grid.add(createText("Default"), 2, 1); grid.addRow(3, title4Text, createFontSizeLabel());
grid.add(createFontSizeLabel(), 3, 1);
grid.add(createText("Small", TEXT_SMALL), 2, 2);
grid.add(createFontSizeLabel(), 3, 2);
grid.setAlignment(Pos.BASELINE_LEFT); grid.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Font Size", grid); fontSizeGridPane = grid;
var example = new ExampleBox(grid, new Snippet(getClass(), 1));
example.setAllowDisable(false);
return example;
} }
private SampleBlock fontWeightSample() { private ExampleBox fontWeightExample() {
//snippet_2:start
class StyledText extends Text {
public StyledText(String text, String style) {
super();
setText(text);
setStyle(style + "-fx-fill:-color-fg-default;");
}
}
var boldText = new Text("Bold");
boldText.getStyleClass().addAll(Styles.TEXT_BOLD);
var bolderText = new Text("Bolder");
bolderText.getStyleClass().addAll(Styles.TEXT_BOLDER);
var normalText = new Text("Normal");
normalText.getStyleClass().addAll(Styles.TEXT_NORMAL);
var lighterText = new Text("Lighter");
lighterText.getStyleClass().addAll(Styles.TEXT_LIGHTER);
var sample1 = new HBox( var sample1 = new HBox(
BLOCK_HGAP, HGAP_20, boldText, bolderText, normalText, lighterText
createText("Bold", TEXT_BOLD),
createText("Bolder", TEXT_BOLDER),
createText("Normal", TEXT_NORMAL),
createText("Lighter", TEXT_LIGHTER)
); );
sample1.setAlignment(Pos.BASELINE_LEFT); sample1.setAlignment(Pos.BASELINE_LEFT);
// ~
var sample2 = new HBox( var sample2 = new HBox(
BLOCK_HGAP, HGAP_20,
createStyledText("900", "-fx-font-weight:900;"), new StyledText("900", "-fx-font-weight:900;"),
createStyledText("800", "-fx-font-weight:800;"), new StyledText("800", "-fx-font-weight:800;"),
createStyledText("700", "-fx-font-weight:700;"), new StyledText("700", "-fx-font-weight:700;"),
createStyledText("600", "-fx-font-weight:600;"), new StyledText("600", "-fx-font-weight:600;"),
createStyledText("500", "-fx-font-weight:500;"), new StyledText("500", "-fx-font-weight:500;"),
createStyledText("400", "-fx-font-weight:400;"), new StyledText("400", "-fx-font-weight:400;"),
createStyledText("300", "-fx-font-weight:300;"), new StyledText("300", "-fx-font-weight:300;"),
createStyledText("200", "-fx-font-weight:200;"), new StyledText("200", "-fx-font-weight:200;"),
createStyledText("100", "-fx-font-weight:100;") new StyledText("100", "-fx-font-weight:100;"),
new Text("\uD83E\uDC60 no difference")
); );
sample2.setAlignment(Pos.BASELINE_LEFT); sample2.setAlignment(Pos.BASELINE_LEFT);
// JDK-8090423:
// https://bugs.openjdk.org/browse/JDK-8090423
// Workaround:
// https://edencoding.com/resources/css_properties/fx-font-weight/
var sample3 = new HBox( var sample3 = new HBox(
BLOCK_HGAP, HGAP_20,
createStyledText("900", "-fx-font-family:'Inter Black';"), new StyledText("900", "-fx-font-family:'Inter Black';"),
createStyledText("800", "-fx-font-family:'Inter Extra Bold';"), new StyledText("800", "-fx-font-family:'Inter Extra Bold';"),
createStyledText("700", "-fx-font-family:'Inter Bold';"), new StyledText("700", "-fx-font-family:'Inter Bold';"),
createStyledText("600", "-fx-font-family:'Inter Semi Bold';"), new StyledText("600", "-fx-font-family:'Inter Semi Bold';"),
createStyledText("500", "-fx-font-family:'Inter Medium';"), new StyledText("500", "-fx-font-family:'Inter Medium';"),
createStyledText("400", "-fx-font-family:'Inter Regular';"), new StyledText("400", "-fx-font-family:'Inter Regular';"),
createStyledText("300", "-fx-font-family:'Inter Light';"), new StyledText("300", "-fx-font-family:'Inter Light';"),
createStyledText("200", "-fx-font-family:'Inter Extra Light';"), new StyledText("200", "-fx-font-family:'Inter Extra Light';"),
createStyledText("100", "-fx-font-family:'Inter Thin';") new StyledText("100", "-fx-font-family:'Inter Thin';"),
new Text("\uD83E\uDC60 workaround")
); );
sample3.setAlignment(Pos.BASELINE_LEFT); sample3.setAlignment(Pos.BASELINE_LEFT);
//snippet_2:end
// JDK-8090423: https://bugs.openjdk.org/browse/JDK-8090423 var box = new VBox(VGAP_20, sample1, sample2, sample3);
// Workaround: https://edencoding.com/resources/css_properties/fx-font-weight/ box.setAlignment(Pos.CENTER_LEFT);
return new SampleBlock("Font Weight", new VBox(
BLOCK_VGAP, var description = BBCodeParser.createFormattedText("""
sample1, JavaFX [color="-color-danger-fg"]only supports Bold or Regular[/color] font weight. \
sample2, See the source code for workaround."""
sample3, );
createText("JavaFX only supports Bold or Regular font weight. See the source code for workaround.", TEXT,
WARNING) var example = new ExampleBox(box, new Snippet(getClass(), 2), description);
)); example.setAllowDisable(false);
return example;
} }
private SampleBlock fontStyleSample() { private ExampleBox fontStyleExample() {
//snippet_3:start
var italicText = new Text("Italic");
italicText.getStyleClass().addAll(
Styles.TEXT, Styles.TEXT_ITALIC
);
var obliqueText = new Text("Oblique");
obliqueText.getStyleClass().addAll(
Styles.TEXT, Styles.TEXT_OBLIQUE
);
var underlinedText = new Text("Underlined");
underlinedText.getStyleClass().addAll(
Styles.TEXT, Styles.TEXT_UNDERLINED
);
var strikethroughText = new Text("Strikethrough");
strikethroughText.getStyleClass().addAll(
Styles.TEXT, Styles.TEXT_STRIKETHROUGH
);
//snippet_3:end
var box = new FlowPane( var box = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP, HGAP_20, VGAP_20,
createText("Italic", TEXT_ITALIC), italicText, obliqueText, underlinedText, strikethroughText
createText("Oblique", TEXT_OBLIQUE),
createText("Underlined", TEXT_UNDERLINED),
createText("Strikethrough", TEXT_STRIKETHROUGH)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Font Style", box); var example = new ExampleBox(box, new Snippet(getClass(), 3));
example.setAllowDisable(false);
return example;
} }
private SampleBlock textColorSample() { private ExampleBox textColorExample() {
//snippet_4:start
var accentText = new Text("Accent");
accentText.getStyleClass().addAll(Styles.TEXT, Styles.ACCENT);
var successText = new Text("Success");
successText.getStyleClass().addAll(Styles.TEXT, Styles.SUCCESS);
var warningText = new Text("Warning");
warningText.getStyleClass().addAll(Styles.TEXT, Styles.WARNING);
var dangerText = new Text("Danger");
dangerText.getStyleClass().addAll(Styles.TEXT, Styles.DANGER);
var mutedText = new Text("Muted");
mutedText.getStyleClass().addAll(Styles.TEXT, Styles.TEXT_MUTED);
var subtleText = new Text("Subtle");
subtleText.getStyleClass().addAll(Styles.TEXT, Styles.TEXT_SUBTLE);
//snippet_4:end
var box = new FlowPane( var box = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP, HGAP_20, VGAP_20,
createText("Accent", TEXT, ACCENT), accentText, successText, warningText, dangerText, mutedText, subtleText
createText("Success", TEXT, SUCCESS),
createText("Warning", TEXT, WARNING),
createText("Danger", TEXT, DANGER),
createText("Muted", TEXT, TEXT_MUTED),
createText("Subtle", TEXT, TEXT_SUBTLE)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Text Color", box); var example = new ExampleBox(box, new Snippet(getClass(), 4));
example.setAllowDisable(false);
return example;
} }
private SampleBlock hyperlinkSample() { private ExampleBox hyperlinkExample() {
var linkNormal = createHyperlink("_Normal", false, false); //snippet_5:start
var linkNormal = new Hyperlink("_Normal");
linkNormal.setMnemonicParsing(true); linkNormal.setMnemonicParsing(true);
var linkVisited = createHyperlink("_Visited", true, false); var linkVisited = new Hyperlink("_Visited");
linkVisited.setVisited(true);
linkVisited.setMnemonicParsing(true); linkVisited.setMnemonicParsing(true);
var linkBroken = createHyperlink("_Broken", true, false); var linkBroken = new Hyperlink("_Broken");
linkBroken.setStyle("-color-link-fg-visited:-color-danger-fg;"); linkBroken.setStyle("-color-link-fg-visited:-color-danger-fg;");
linkBroken.setVisited(true);
linkBroken.setMnemonicParsing(true); linkBroken.setMnemonicParsing(true);
var linkDisabled = new Hyperlink("Disabled");
linkDisabled.setDisable(true);
//snippet_5:end
var box = new FlowPane( var box = new FlowPane(
BLOCK_HGAP, BLOCK_VGAP, HGAP_20, VGAP_20,
linkNormal, linkNormal, linkVisited, linkBroken, linkDisabled
linkVisited,
linkBroken,
createHyperlink("Disabled", false, true)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
return new SampleBlock("Hyperlink", box); var example = new ExampleBox(box, new Snippet(getClass(), 5));
} example.setAllowDisable(false);
private SampleBlock textFlowSample() { return example;
var textFlow = new TextFlow(
new Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. "),
new Hyperlink("Vivamus at lorem"),
new Text(" in urna facilisis aliquam. Morbi ut "),
new Hyperlink("velit"),
new Text(" iaculis erat cursus molestie eget laoreet quam. "),
new Text(" Vivamus eu nulla sapien. Sed et malesuada augue. Nullam nec "),
new Hyperlink("consectetur"),
new Text(" "),
new Hyperlink("ipsum"),
new Text(", eget facilisis enim. Suspendisse potenti. Nulla euismod, nisl sed dapibus pretium,"
+ " augue ligula finibus arcu, in iaculis nulla neque a est. Sed in rutrum diam."
+ " Donec quis arcu molestie, facilisis ex fringilla, "),
new Hyperlink("volutpat velit"),
new Text(".")
);
return new SampleBlock("Text Flow", textFlow);
}
private Text createText(String text, String... styleClasses) {
var t = new Text(text);
t.getStyleClass().addAll(styleClasses);
t.setUserData(text);
return t;
}
private Text createStyledText(String text, String style) {
var t = new Text(text);
t.setStyle(style);
return t;
} }
private Label createFontSizeLabel() { private Label createFontSizeLabel() {
var label = new Label(); var label = new Label();
label.setPadding(new Insets(5, 40, 5, 10)); label.setPadding(new Insets(5, 40, 5, 10));
label.setStyle("-fx-font-family:monospace;");
return label; return label;
} }
private Hyperlink createHyperlink(String text, boolean visited, boolean disabled) {
var h = new Hyperlink(text);
h.setVisited(visited);
h.setDisable(disabled);
return h;
}
} }

@ -1,68 +1,114 @@
package atlantafx.sampler.page.showcase; package atlantafx.sampler.page.showcase;
import static atlantafx.base.theme.Styles.ACCENT; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.Page;
import atlantafx.sampler.util.NodeUtils; import java.util.Objects;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Insets; import javafx.scene.Node;
import javafx.geometry.Pos;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.Button; import javafx.scene.control.Label;
import javafx.scene.control.Hyperlink;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.StageStyle; import javafx.stage.StageStyle;
import org.jetbrains.annotations.Nullable;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
public abstract class ShowcasePage extends AbstractPage { public abstract class ShowcasePage extends StackPane implements Page {
protected static final PseudoClass SHOWCASE_MODE = PseudoClass.getPseudoClass("showcase-mode"); protected static final PseudoClass SHOWCASE_MODE = PseudoClass.getPseudoClass("showcase-mode");
protected final StackPane showcase = new StackPane(); protected static final int DEFAULT_WIDTH = 800;
protected final HBox expandBox = new HBox(10); protected static final int DEFAULT_HEIGHT = 600;
protected final HBox collapseBox = new HBox(10);
protected VBox showcaseWindow = new VBox();
protected Label windowTitle = new Label();
protected VBox showCaseContent = new VBox();
protected int windowWidth = DEFAULT_WIDTH;
protected int windowHeight = DEFAULT_HEIGHT;
protected BooleanProperty maximized = new SimpleBooleanProperty();
public ShowcasePage() { public ShowcasePage() {
super();
createShowcaseLayout(); createShowcaseLayout();
} }
protected void createShowcaseLayout() {
windowTitle.getStyleClass().addAll("title");
var maximizeBtn = new FontIcon(Feather.MAXIMIZE_2);
maximizeBtn.getStyleClass().addAll(Styles.SMALL, Styles.FLAT);
maximizeBtn.setOnMouseClicked(e -> maximized.set(!maximized.get()));
var windowHeader = new HBox(windowTitle, new Spacer(), maximizeBtn);
windowHeader.getStyleClass().add("header");
showcaseWindow.getStyleClass().add("window");
showcaseWindow.getChildren().setAll(windowHeader, showCaseContent);
VBox.setVgrow(showCaseContent, Priority.ALWAYS);
getStyleClass().add("showcase-page");
getChildren().setAll(showcaseWindow);
maximized.addListener((obs, old, val) -> {
getScene().getRoot().pseudoClassStateChanged(SHOWCASE_MODE, val);
maximizeBtn.setIconCode(val ? Feather.MINIMIZE_2 : Feather.MAXIMIZE_2);
var width = val ? -1 : windowWidth;
var height = val ? -1 : windowHeight;
showcaseWindow.setMinSize(width, height);
showcaseWindow.setMaxSize(width, height);
});
}
protected void setWindowTitle(String text, @Nullable Node graphic) {
windowTitle.setText(Objects.requireNonNull(text, "text"));
if (graphic != null) {
windowTitle.setGraphic(graphic);
}
}
protected void setShowCaseContent(Node node) {
setShowCaseContent(node, DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
protected void setShowCaseContent(Node node, int width, int height) {
Objects.requireNonNull(node, "node");
this.windowWidth = width > 0 ? width : DEFAULT_WIDTH;
this.windowHeight = height > 0 ? height : DEFAULT_HEIGHT;
showcaseWindow.setMinSize(width, height);
showcaseWindow.setMaxSize(width, height);
showCaseContent.getChildren().setAll(node);
VBox.setVgrow(node, Priority.ALWAYS);
}
@Override
public Pane getView() {
return this;
}
@Override @Override
public boolean canDisplaySourceCode() { public boolean canDisplaySourceCode() {
return false; return false;
} }
protected void createShowcaseLayout() { @Override
var expandBtn = new Button("Expand"); public boolean canChangeThemeSettings() {
expandBtn.setGraphic(new FontIcon(Feather.MAXIMIZE_2)); return true;
expandBtn.getStyleClass().add(ACCENT); }
expandBtn.setOnAction(e -> {
expandBtn.getScene().getRoot().pseudoClassStateChanged(SHOWCASE_MODE, true); @Override
VBox.setVgrow(showcase, Priority.ALWAYS); public void reset() {
NodeUtils.toggleVisibility(expandBox, false);
NodeUtils.toggleVisibility(collapseBox, true);
});
expandBox.getChildren().setAll(expandBtn);
expandBox.setAlignment(Pos.CENTER);
var collapseBtn = new Hyperlink("Exit showcase mode");
collapseBtn.setOnAction(e -> {
expandBtn.getScene().getRoot().pseudoClassStateChanged(SHOWCASE_MODE, false);
VBox.setVgrow(showcase, Priority.NEVER);
NodeUtils.toggleVisibility(expandBox, true);
NodeUtils.toggleVisibility(collapseBox, false);
});
collapseBox.getChildren().setAll(new FontIcon(Feather.MINIMIZE_2), collapseBtn);
collapseBox.setAlignment(Pos.CENTER_LEFT);
collapseBox.setPadding(new Insets(5));
NodeUtils.toggleVisibility(collapseBox, false);
setUserContent(new VBox(showcase, expandBox, collapseBox));
} }
@SuppressWarnings("SameParameterValue")
protected void showWarning(String header, String description) { protected void showWarning(String header, String description) {
var alert = new Alert(Alert.AlertType.WARNING); var alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Error Dialog"); alert.setTitle("Error Dialog");

@ -2,6 +2,7 @@ package atlantafx.sampler.page.showcase.filemanager;
import static atlantafx.sampler.page.showcase.filemanager.Model.USER_HOME; import static atlantafx.sampler.page.showcase.filemanager.Model.USER_HOME;
import atlantafx.base.theme.Tweaks;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
@ -14,7 +15,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
final class BookmarkList extends ListView<Bookmark> { final class BookmarkList extends ListView<Bookmark> {
public BookmarkList(Model model) { public BookmarkList(Model model) {
getStyleClass().add("bookmark-list"); getStyleClass().addAll("bookmark-list", Tweaks.EDGE_TO_EDGE);
// this is Linux specific and only for EN locale // this is Linux specific and only for EN locale
getItems().setAll( getItems().setAll(

@ -7,7 +7,6 @@ import static atlantafx.base.theme.Styles.FLAT;
import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_ANY; import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_ANY;
import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_NOT_HIDDEN; import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_NOT_HIDDEN;
import static atlantafx.sampler.page.showcase.filemanager.Utils.openFile; import static atlantafx.sampler.page.showcase.filemanager.Utils.openFile;
import static atlantafx.sampler.util.Controls.hyperlink;
import static atlantafx.sampler.util.Controls.iconButton; import static atlantafx.sampler.util.Controls.iconButton;
import atlantafx.base.controls.Breadcrumbs; import atlantafx.base.controls.Breadcrumbs;
@ -16,7 +15,6 @@ import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Tweaks; import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.showcase.ShowcasePage; import atlantafx.sampler.page.showcase.ShowcasePage;
import atlantafx.sampler.util.Containers; import atlantafx.sampler.util.Containers;
import java.net.URI;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
@ -24,7 +22,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase; import javafx.scene.control.ButtonBase;
@ -35,9 +32,7 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitPane; import javafx.scene.control.SplitPane;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.util.Callback; import javafx.util.Callback;
import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@ -123,27 +118,12 @@ public class FileManagerPage extends ShowcasePage {
(obs, old, val) -> splitPane.setDividerPosition(0, 200 / splitPane.getWidth()) (obs, old, val) -> splitPane.setDividerPosition(0, 200 / splitPane.getWidth())
); );
var aboutBox = new HBox(new Text(
"Simple file manager. You can traverse through the file system and open files"
+ " via default system application."
));
aboutBox.setPadding(new Insets(5, 0, 5, 0));
aboutBox.getStyleClass().add("about");
var creditsBox = new HBox(5,
new Text("Inspired by ©"),
hyperlink("Gnome Files", URI.create("https://gitlab.gnome.org/GNOME/nautilus"))
);
creditsBox.getStyleClass().add("credits");
creditsBox.setAlignment(Pos.CENTER_RIGHT);
creditsBox.setPadding(new Insets(5, 0, 5, 0));
var root = new BorderPane(); var root = new BorderPane();
root.getStyleClass().add("file-manager-showcase"); root.getStyleClass().add("file-manager-showcase");
root.getStylesheets().add(STYLESHEET_URL); root.getStylesheets().add(STYLESHEET_URL);
root.setTop(new VBox(aboutBox, topBar)); root.setTop(new VBox(topBar));
root.setCenter(splitPane); root.setCenter(splitPane);
root.setBottom(creditsBox); //root.setBottom(creditsBox);
toggleHiddenCheck.selectedProperty().addListener((obs, old, val) -> directoryView.getFileList() toggleHiddenCheck.selectedProperty().addListener((obs, old, val) -> directoryView.getFileList()
.predicateProperty() .predicateProperty()
@ -164,7 +144,9 @@ public class FileManagerPage extends ShowcasePage {
directoryView.setDirectory(val); directoryView.setDirectory(val);
}); });
showcase.getChildren().setAll(root); setWindowTitle("File Manager", new FontIcon(Material2AL.FOLDER));
setShowCaseContent(root);
Containers.setAnchors(root, Insets.EMPTY); Containers.setAnchors(root, Insets.EMPTY);
} }

@ -74,7 +74,7 @@ final class TableDirectoryView extends AnchorPane implements DirectoryView {
// ~ // ~
var table = new TableView<Path>(); var table = new TableView<Path>();
table.getStyleClass().add(Styles.STRIPED); table.getStyleClass().addAll(Styles.STRIPED, Tweaks.EDGE_TO_EDGE);
table.getColumns().setAll(filenameCol, sizeCol, mtimeCol); table.getColumns().setAll(filenameCol, sizeCol, mtimeCol);
table.getSortOrder().add(filenameCol); table.getSortOrder().add(filenameCol);
table.setSortPolicy(param -> true); table.setSortPolicy(param -> true);

@ -1,16 +1,9 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
.file-manager-showcase .bookmark-list {
-fx-border-width: 0 0 1 1;
}
.file-manager-showcase .bookmark-list .ikonli-font-icon { .file-manager-showcase .bookmark-list .ikonli-font-icon {
-fx-icon-size: 18px; -fx-icon-size: 18px;
} }
.file-manager-showcase .tool-bar {
-fx-background-insets: 0, 1;
}
.file-manager-showcase .breadcrumbs >.divider { .file-manager-showcase .breadcrumbs >.divider {
-fx-padding: 0; -fx-padding: 0;
} }
@ -19,7 +12,6 @@
-color-header-bg: -color-bg-default; -color-header-bg: -color-bg-default;
-color-cell-bg-selected: -color-accent-emphasis; -color-cell-bg-selected: -color-accent-emphasis;
-color-cell-fg-selected: -color-fg-emphasis; -color-cell-fg-selected: -color-fg-emphasis;
-fx-border-width: 0 1 1 0;
} }
/* setting opacity directly to the .table-cell or .table-row-cell /* setting opacity directly to the .table-cell or .table-row-cell

@ -2,23 +2,17 @@
package atlantafx.sampler.page.showcase.musicplayer; package atlantafx.sampler.page.showcase.musicplayer;
import static atlantafx.sampler.util.Controls.hyperlink;
import atlantafx.sampler.page.showcase.ShowcasePage; import atlantafx.sampler.page.showcase.ShowcasePage;
import java.net.URI;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import org.kordamp.ikonli.feather.Feather;
import javafx.scene.text.Text; import org.kordamp.ikonli.javafx.FontIcon;
public class MusicPlayerPage extends ShowcasePage { public class MusicPlayerPage extends ShowcasePage {
public static final String NAME = "Music Player"; public static final String NAME = "Music Player";
public static final double BACKGROUND_OPACITY = 0.2;
public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of("mp3"); public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of("mp3");
private static final String STYLESHEET_URL = Objects.requireNonNull( private static final String STYLESHEET_URL = Objects.requireNonNull(
MusicPlayerPage.class.getResource("music-player.css")).toExternalForm(); MusicPlayerPage.class.getResource("music-player.css")).toExternalForm();
@ -36,26 +30,11 @@ public class MusicPlayerPage extends ShowcasePage {
} }
private void createView() { private void createView() {
var aboutBox = new HBox(new Text("Simple music player that able to play MP3 files."));
aboutBox.setPadding(new Insets(5, 0, 5, 0));
aboutBox.getStyleClass().add("about");
var creditsBox = new HBox(5,
new Text("Inspired by ©"),
hyperlink("Amberol", URI.create("https://gitlab.gnome.org/World/amberol"))
);
creditsBox.getStyleClass().add("credits");
creditsBox.setAlignment(Pos.CENTER_RIGHT);
creditsBox.setPadding(new Insets(5, 0, 5, 0));
// ~
var startScreen = new StartScreen(model); var startScreen = new StartScreen(model);
var playerScreen = new PlayerScreen(model); var playerScreen = new PlayerScreen(model);
var root = new BorderPane(); var root = new BorderPane();
root.setCenter(startScreen); root.setCenter(startScreen);
root.setTop(aboutBox);
root.setBottom(creditsBox);
root.getStylesheets().add(STYLESHEET_URL); root.getStylesheets().add(STYLESHEET_URL);
root.getStyleClass().add("music-player-showcase"); root.getStyleClass().add("music-player-showcase");
@ -68,7 +47,8 @@ public class MusicPlayerPage extends ShowcasePage {
} }
}); });
showcase.getChildren().setAll(root); setWindowTitle("Music Player", new FontIcon(Feather.MUSIC));
setShowCaseContent(root);
} }
@Override @Override

@ -1,160 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import atlantafx.base.theme.Styles;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
public class Card extends VBox {
public static final String CSS = """
.card {
-fx-background-color: -color-bg-default;
}
.card > .subtitle {
-fx-text-fill: -color-fg-muted;
-fx-padding: 0px 15px 10px 15px;
}
.card > .title,
.card > .body,
.card > .footer {
-fx-padding: 10px 15px 10px 15px;
}
""";
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<ImageView> image = new SimpleObjectProperty<>();
private final ObjectProperty<Parent> body = new SimpleObjectProperty<>();
private final ObjectProperty<Parent> footer = new SimpleObjectProperty<>();
public Card() {
super();
createView();
}
private void createView() {
var footerSep = new Separator();
footerSep.getStyleClass().add(Styles.SMALL);
footerSep.managedProperty().bind(Bindings.createObjectBinding(
() -> footer.get() != null && footer.get().isManaged(), footer
));
getChildren().setAll(
createPlaceholder(), // title
createPlaceholder(), // subtitle
createPlaceholder(), // image
createPlaceholder(), // body
footerSep,
createPlaceholder() // footer
);
image.addListener(
(obs, old, val) -> setChild(0, val, "image")
);
title.addListener(
(obs, old, val) -> setChild(1, val != null ? new Label(val) : null, "title", Styles.TITLE_4)
);
subtitle.addListener(
(obs, old, val) -> setChild(2, val != null ? new Label(val) : null, "subtitle")
);
body.addListener(
(obs, old, val) -> setChild(3, val, "body")
);
footer.addListener(
(obs, old, val) -> setChild(5, val, "footer")
);
getStyleClass().addAll("card", Styles.BORDERED);
}
private void setChild(int index, Node node, String... styleClass) {
if (node != null) {
for (var s : styleClass) {
if (!node.getStyleClass().contains(s)) {
node.getStyleClass().add(s);
}
}
getChildren().set(index, node);
} else {
getChildren().set(index, createPlaceholder());
}
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public String getSubtitle() {
return subtitle.get();
}
public StringProperty subtitleProperty() {
return subtitle;
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public ImageView getImage() {
return image.get();
}
public ObjectProperty<ImageView> imageProperty() {
return image;
}
public void setImage(ImageView image) {
this.image.set(image);
}
public Parent getBody() {
return body.get();
}
public ObjectProperty<Parent> bodyProperty() {
return body;
}
public void setBody(Parent body) {
this.body.set(body);
}
public Parent getFooter() {
return footer.get();
}
public ObjectProperty<Parent> footerProperty() {
return footer;
}
public void setFooter(Parent footer) {
this.footer.set(footer);
}
private Parent createPlaceholder() {
var g = new Group();
g.setManaged(false);
return g;
}
}

@ -1,180 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import static atlantafx.sampler.page.Page.PAGE_HGAP;
import static atlantafx.sampler.page.Page.PAGE_VGAP;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.Resources;
import atlantafx.sampler.theme.CSSFragment;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelReader;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import net.datafaker.Faker;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class CardSample extends HBox {
private static final Faker FAKER = new Faker();
private static final int CARD_WIDTH = 300;
public CardSample() {
new CSSFragment(Card.CSS).addTo(this);
setSpacing(PAGE_HGAP);
setAlignment(Pos.TOP_CENTER);
setMinWidth(CARD_WIDTH * 2 + PAGE_HGAP);
getChildren().setAll(
// column 0
new VBox(
PAGE_VGAP,
textFooterCard(),
titleTextCard(),
quoteCard()
),
// column 1
new VBox(
PAGE_VGAP,
imageTextCard(),
titleImageCard(),
statisticCard()
)
);
}
private Card textFooterCard() {
var card = new Card();
card.getStyleClass().add(Styles.ELEVATED_1);
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
var text = new Text(FAKER.chuckNorris().fact());
card.setBody(new TextFlow(text));
var btn = new Button("More!");
btn.getStyleClass().addAll(Styles.ACCENT, Styles.BUTTON_OUTLINED);
btn.setOnAction(e -> text.setText(FAKER.chuckNorris().fact()));
card.setFooter(new HBox(btn));
return card;
}
private Card imageTextCard() {
var card = new Card();
card.getStyleClass().add(Styles.ELEVATED_4);
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
var image = new ImageView(new Image(Resources.getResourceAsStream("images/20_min_adventure.jpg")));
image.setFitWidth(300);
image.setPreserveRatio(true);
card.setImage(image);
var text = new Text(FAKER.rickAndMorty().quote());
card.setBody(new TextFlow(text));
return card;
}
private Card titleTextCard() {
var card = new Card();
card.getStyleClass().add(Styles.INTERACTIVE);
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
card.setTitle("Interactive");
card.setSubtitle("Subtitle");
var text = new Text(FAKER.lorem().paragraph());
card.setBody(new TextFlow(text));
return card;
}
private Card titleImageCard() {
var card = new Card();
card.getStyleClass().add(Styles.ELEVATED_2);
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
var image = new Image(Resources.getResourceAsStream("images/pattern.jpg"));
PixelReader pixelReader = image.getPixelReader();
var cropImage = new WritableImage(pixelReader, 0, 0, 300, 100);
card.setImage(new ImageView(cropImage));
card.setTitle("Title");
var text = new Text(FAKER.lorem().sentence());
card.setBody(new TextFlow(text));
return card;
}
private Card quoteCard() {
var card = new Card();
card.getStyleClass().add(Styles.ELEVATED_3);
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
var quoteText = new Text(FAKER.bojackHorseman().quotes());
quoteText.getStyleClass().add(Styles.TITLE_3);
var authorText = new Text("Bojack Horseman");
card.setBody(new VBox(
10,
new TextFlow(quoteText),
authorText
));
card.setFooter(new TextFlow(
new Text("Share on "),
new Hyperlink("Twitter")
));
return card;
}
private Card statisticCard() {
var card = new Card();
card.setMinWidth(CARD_WIDTH);
card.setMaxWidth(CARD_WIDTH);
card.setTitle("Statistic");
var grid = new GridPane();
grid.setHgap(40);
grid.setVgap(10);
card.setBody(grid);
var leftHead = new Text("Active");
leftHead.getStyleClass().add(Styles.TEXT_MUTED);
grid.add(leftHead, 0, 0);
var leftData = new Label("12.87", new FontIcon(Material2AL.ARROW_UPWARD));
leftData.getStyleClass().addAll(Styles.SUCCESS, Styles.TITLE_2);
grid.add(leftData, 0, 1);
var rightHead = new Text("Idle");
rightHead.getStyleClass().add(Styles.TEXT_MUTED);
grid.add(rightHead, 1, 0);
var rightData = new Label("3.74", new FontIcon(Material2AL.ARROW_DOWNWARD));
rightData.getStyleClass().addAll(Styles.DANGER, Styles.TITLE_2);
grid.add(rightData, 1, 1);
return card;
}
}

@ -1,147 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import atlantafx.base.theme.Styles;
import java.util.Objects;
import java.util.function.Consumer;
import javafx.animation.FadeTransition;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.util.Duration;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class Message extends StackPane {
private static final int ANIMATION_DURATION = 500;
public enum Type {
INFO, SUCCESS, WARNING, DANGER
}
public static final String CSS = """
.message {
-color-message-bg: -color-bg-default;
-color-message-fg: -color-fg-default;
-fx-background-color: -color-message-bg;
-fx-border-color: -color-message-fg;
-fx-border-width: 0 0 0 5px;
-fx-pref-width: 600px;
-fx-alignment: TOP_LEFT;
}
.message > .header {
-fx-font-weight: bold;
}
.message Text {
-fx-fill: -color-message-fg;
}
.message > .button {
-color-button-fg: -color-message-fg;
}
.message.info {
-color-message-bg: -color-accent-subtle;
-color-message-fg: -color-accent-fg;
}
.message.success {
-color-message-bg: -color-success-subtle;
-color-message-fg: -color-success-fg;
}
.message.warning {
-color-message-bg: -color-warning-subtle;
-color-message-fg: -color-warning-fg;
}
.message.danger {
-color-message-bg: -color-danger-subtle;
-color-message-fg: -color-danger-fg;
}
""";
private final Type type;
private final String header;
private final String text;
private Consumer<Message> closeHandler;
public Message(Type type, String header, String text) {
this.type = Objects.requireNonNull(type);
this.header = header;
this.text = Objects.requireNonNull(text);
createView();
}
private void createView() {
if (header != null) {
var headerText = new Text(header);
headerText.getStyleClass().addAll("header");
StackPane.setMargin(headerText, new Insets(10, 10, 0, 15));
getChildren().add(headerText);
}
var messageText = new TextFlow(new Text(text));
if (header != null) {
StackPane.setMargin(messageText, new Insets(40, 10, 10, 15));
} else {
StackPane.setMargin(messageText, new Insets(10, 10, 10, 15));
}
messageText.maxWidthProperty().bind(widthProperty().subtract(50));
getChildren().add(messageText);
var closeBtn = new Button("", new FontIcon(Material2AL.CLOSE));
closeBtn.getStyleClass().addAll(Styles.BUTTON_CIRCLE, Styles.FLAT);
closeBtn.setOnAction(e -> handleClose());
StackPane.setMargin(closeBtn, new Insets(2));
StackPane.setAlignment(closeBtn, Pos.TOP_RIGHT);
getChildren().add(closeBtn);
parentProperty().addListener((obs, old, val) -> {
if (val != null) {
handleOpen();
}
});
getStyleClass().setAll("message", type.name().toLowerCase());
}
public Type getType() {
return type;
}
public String getHeader() {
return header;
}
public String getText() {
return text;
}
public void setCloseHandler(Consumer<Message> closeHandler) {
this.closeHandler = closeHandler;
}
private void handleOpen() {
var transition = new FadeTransition(new Duration(500), this);
transition.setFromValue(0);
transition.setToValue(1);
transition.play();
}
private void handleClose() {
var transition = new FadeTransition(new Duration(ANIMATION_DURATION), this);
transition.setFromValue(1);
transition.setToValue(0);
transition.setOnFinished(e -> {
if (getParent() != null && getParent() instanceof Pane pane) {
pane.getChildren().remove(this);
}
if (closeHandler != null) {
closeHandler.accept(this);
}
});
transition.play();
}
}

@ -1,42 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import java.util.function.Consumer;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
public class MessageSample extends SampleBlock {
public MessageSample() {
super("Message", createContent());
}
private static Node createContent() {
var content = new VBox(BLOCK_VGAP);
content.setAlignment(Pos.TOP_CENTER);
VBox.setVgrow(content, Priority.ALWAYS);
new CSSFragment(Message.CSS).addTo(content);
var closeHandler = new Consumer<Message>() {
@Override
public void accept(Message msg) {
var newMsg = new Message(msg.getType(), msg.getHeader(), FAKER.chuckNorris().fact());
newMsg.setCloseHandler(this);
content.getChildren().add(newMsg);
}
};
for (Message.Type type : Message.Type.values()) {
var msg = new Message(type, type.name(), FAKER.chuckNorris().fact());
msg.setCloseHandler(closeHandler);
content.getChildren().add(msg);
}
return content;
}
}

@ -1,259 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon;
public class Stepper extends HBox {
public static final String CSS = """
.stepper {
-color-stepper-bg: -color-bg-subtle;
-color-stepper-fg: -color-fg-default;
-color-stepper-border: -color-border-default;
-fx-pref-width: 600px;
-fx-spacing: 10px;
}
.stepper > .item {
-fx-graphic-text-gap: 10px;
}
.stepper > .item > .graphic {
-fx-font-size: 1.1em;
-fx-min-width: 2.2em;
-fx-max-width: 2.2em;
-fx-min-height: 2.2em;
-fx-max-height: 2.2em;
-fx-text-fill: -color-stepper-fg;
-fx-background-color: -color-stepper-bg;
-fx-background-radius: 10em;
-fx-border-color: -color-stepper-border;
-fx-border-radius: 10em;
-fx-border-width: 1;
-fx-alignment: CENTER;
}
.stepper > .item .ikonli-font-icon {
-fx-fill: -color-stepper-fg;
-fx-icon-color: -color-stepper-fg;
}
.stepper > .item:selected {
-color-stepper-bg: -color-bg-default;
-color-stepper-fg: -color-accent-fg;
-color-stepper-border: -color-accent-emphasis;
}
.stepper > .item:selected > .graphic {
-color-stepper-bg: -color-bg-default;
-color-stepper-fg: -color-accent-fg;
-color-stepper-border: -color-accent-emphasis;
-fx-border-width: 2;
}
.stepper > .item:completed {
-color-stepper-bg: -color-accent-emphasis;
-color-stepper-fg: -color-fg-emphasis;
-color-stepper-border: -color-accent-emphasis;
}
""";
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private static final PseudoClass COMPLETED = PseudoClass.getPseudoClass("completed");
private final List<Item> items;
private final ObjectProperty<Side> textPosition = new SimpleObjectProperty<>(Side.LEFT);
private final ObjectProperty<Item> selectedItem = new SimpleObjectProperty<>();
private final BooleanBinding canGoBack;
private final BooleanBinding canGoForward;
public Stepper(Item... items) {
this(Arrays.asList(items));
}
public Stepper(List<Item> items) {
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("Item list can't be null or empty.");
}
this.items = Collections.unmodifiableList(items);
canGoBack = Bindings.createBooleanBinding(() -> {
if (selectedItem.get() == null) {
return false;
}
var idx = items.indexOf(selectedItem.get());
return idx > 0 && idx <= items.size() - 1;
}, selectedItem);
canGoForward = Bindings.createBooleanBinding(() -> {
if (selectedItem.get() == null) {
return false;
}
var idx = items.indexOf(selectedItem.get());
return idx >= 0 && idx < items.size() - 1;
}, selectedItem);
selectedItem.addListener((obs, old, val) -> {
if (old != null) {
old.pseudoClassStateChanged(SELECTED, false);
}
if (val != null) {
val.pseudoClassStateChanged(SELECTED, true);
}
});
createView();
}
private void createView() {
alignmentProperty().bind(Bindings.createObjectBinding(
() -> switch (textPositionProperty().get()) {
case TOP -> Pos.TOP_LEFT;
case BOTTOM -> Pos.BOTTOM_LEFT;
default -> Pos.CENTER_LEFT;
}, textPositionProperty())
);
updateItems();
getStyleClass().add("stepper");
}
private void updateItems() {
var children = new ArrayList<Node>();
for (int i = 0; i < items.size(); i++) {
var item = items.get(i);
item.contentDisplayProperty().bind(Bindings.createObjectBinding(
() -> switch (textPositionProperty().get()) {
case TOP -> ContentDisplay.TOP;
case BOTTOM -> ContentDisplay.BOTTOM;
case LEFT -> ContentDisplay.LEFT;
case RIGHT -> ContentDisplay.RIGHT;
}, textPositionProperty())
);
children.add(item);
if (i < items.size() - 1) {
var sep = new Separator();
HBox.setHgrow(sep, Priority.ALWAYS);
children.add(sep);
}
}
getChildren().setAll(children);
}
public List<Item> getItems() {
return items;
}
public Side getTextPosition() {
return textPosition.get();
}
public void setTextPosition(Side textPosition) {
this.textPosition.set(textPosition);
}
public ObjectProperty<Side> textPositionProperty() {
return textPosition;
}
public Item getSelectedItem() {
return selectedItem.get();
}
public ObjectProperty<Item> selectedItemProperty() {
return selectedItem;
}
public void setSelectedItem(Item selectedItem) {
this.selectedItem.set(selectedItem);
}
public BooleanBinding canGoBackProperty() {
return canGoBack;
}
public void backward() {
if (!canGoBack.get()) {
return;
}
var idx = items.indexOf(selectedItem.get());
selectedItem.set(items.get(idx - 1));
}
public BooleanBinding canGoForwardProperty() {
return canGoForward;
}
public void forward() {
if (!canGoForward.get()) {
return;
}
var idx = items.indexOf(selectedItem.get());
selectedItem.set(items.get(idx + 1));
}
///////////////////////////////////////////////////////////////////////////
public static class Item extends Label {
private final BooleanProperty completed = new SimpleBooleanProperty();
public Item(String text) {
super(text);
var graphicLabel = new Label();
graphicLabel.getStyleClass().add("graphic");
setGraphic(graphicLabel);
completed.addListener((obs, old, val) -> pseudoClassStateChanged(COMPLETED, val));
getStyleClass().add("item");
setContentDisplay(ContentDisplay.LEFT);
}
public void setGraphic(Ikon icon) {
var graphicLabel = ((Label) getGraphic());
if (icon != null) {
graphicLabel.setText(null);
graphicLabel.setGraphic(new FontIcon(icon));
}
}
public void setGraphic(String text) {
var graphicLabel = ((Label) getGraphic());
if (text != null) {
graphicLabel.setText(text);
graphicLabel.setGraphic(null);
}
}
public boolean isCompleted() {
return completed.get();
}
public void setCompleted(boolean state) {
completed.set(state);
}
public BooleanProperty completedProperty() {
return completed;
}
}
}

@ -1,123 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import atlantafx.base.controls.Spacer;
import atlantafx.base.controls.ToggleSwitch;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.page.showcase.widget.Stepper.Item;
import atlantafx.sampler.theme.CSSFragment;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2MZ;
public class StepperSample extends SampleBlock {
public StepperSample() {
super("Stepper", createContent());
}
private static Node createContent() {
var content = new VBox(BLOCK_VGAP);
new CSSFragment(Stepper.CSS).addTo(content);
// == STEPPER CONTENT ==
var stackContent = new Label();
stackContent.getStyleClass().add(Styles.TITLE_1);
stackContent.setStyle("-fx-background-color:-color-bg-default;");
stackContent.setWrapText(true);
stackContent.setMinHeight(200);
stackContent.setMaxWidth(400);
stackContent.setAlignment(Pos.CENTER);
var stack = new StackPane(stackContent);
stack.setPrefHeight(200);
// == STEPPER ==
var firstItem = new Item("First");
firstItem.setGraphic("A");
var secondItem = new Item("Second");
secondItem.setGraphic("B");
var thirdItem = new Item("Third");
thirdItem.setGraphic("C");
var stepper = new Stepper(firstItem, secondItem, thirdItem);
stepper.selectedItemProperty().addListener(
(obs, old, val) -> stackContent.setText(val != null ? val.getText() : null)
);
stepper.setSelectedItem(stepper.getItems().get(0));
// == CONTROLS ==
var nextBtn = new Button("Next");
nextBtn.setDefaultButton(true);
nextBtn.setOnAction(e -> {
// you can validate user input before moving forward here
stepper.getSelectedItem().setCompleted(true);
stepper.forward();
});
nextBtn.textProperty().bind(Bindings.createStringBinding(
() -> stepper.canGoForwardProperty().get() ? "Next" : "Done", stepper.canGoForwardProperty())
);
var prevBtn = new Button("Previous");
prevBtn.getStyleClass().addAll(Styles.FLAT);
prevBtn.setOnAction(e -> {
stepper.getSelectedItem().setCompleted(false);
stepper.backward();
});
prevBtn.disableProperty().bind(stepper.canGoBackProperty().not());
var iconToggle = new ToggleSwitch("Icons");
iconToggle.selectedProperty().addListener((obs, old, val) -> {
for (int i = 0; i < stepper.getItems().size(); i++) {
var item = stepper.getItems().get(i);
if (val) {
item.setGraphic(randomIcon());
} else {
item.setGraphic(String.valueOf(i + 1));
}
}
});
var rotateBtn = new Button("Rotate", new FontIcon(Material2MZ.ROTATE_RIGHT));
rotateBtn.setOnAction(e -> {
Side nextSide = switch (stepper.getTextPosition()) {
case LEFT -> Side.TOP;
case TOP -> Side.RIGHT;
case RIGHT -> Side.BOTTOM;
case BOTTOM -> Side.LEFT;
};
stepper.setTextPosition(nextSide);
});
var controls = new HBox(
BLOCK_HGAP,
nextBtn,
prevBtn,
new Spacer(),
iconToggle,
rotateBtn
);
controls.setAlignment(Pos.CENTER_LEFT);
// ~
content.getChildren().setAll(stepper, stack, new Separator(), controls);
return content;
}
}

@ -1,52 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import static javafx.scene.control.ContentDisplay.RIGHT;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
public class Tag extends Button {
public static final String CSS = """
.tag {
-fx-padding: 4px 6px 4px 6px;
-fx-cursor: hand;
-color-button-border-hover: -color-button-border;
-color-button-border-focused: -color-button-border;
-color-button-border-pressed: -color-button-border;
}
""";
public Tag(String text) {
this(text, null);
}
public Tag(String text, Node graphic) {
super(text, graphic);
if (graphic != null) {
graphic.setOnMouseEntered(e -> {
if (getContentDisplay() == RIGHT) {
graphic.setScaleX(1.2);
graphic.setScaleY(1.2);
}
});
graphic.setOnMouseExited(e -> {
if (getContentDisplay() == RIGHT) {
graphic.setScaleX(1);
graphic.setScaleY(1);
}
});
graphic.setOnMouseClicked(e -> {
if (getContentDisplay() == RIGHT && getParent() != null && getParent() instanceof Pane pane) {
pane.getChildren().remove(this);
}
});
}
getStyleClass().add("tag");
}
}

@ -1,157 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import static atlantafx.sampler.page.Page.PAGE_HGAP;
import static atlantafx.sampler.page.Page.PAGE_VGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_HGAP;
import static atlantafx.sampler.page.SampleBlock.BLOCK_VGAP;
import atlantafx.base.theme.Styles;
import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.CSSFragment;
import javafx.scene.control.ContentDisplay;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
public class TagSample extends GridPane {
private static final int PREF_WIDTH = 300;
public TagSample() {
new CSSFragment(Tag.CSS).addTo(this);
setHgap(PAGE_HGAP);
setVgap(PAGE_VGAP);
add(filledTagSample(), 0, 0);
add(iconTagSample(), 1, 0);
add(outlinedTagSample(), 0, 1);
add(closeableTagSample(), 1, 1);
add(customColorTagSample(), 0, 2);
}
private SampleBlock filledTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
var basicTag = new Tag("basic");
content.getChildren().add(basicTag);
var accentTag = new Tag("accent");
accentTag.getStyleClass().add(Styles.ACCENT);
content.getChildren().add(accentTag);
var successTag = new Tag("success");
successTag.getStyleClass().add(Styles.SUCCESS);
content.getChildren().add(successTag);
var dangerTag = new Tag("danger");
dangerTag.getStyleClass().add(Styles.DANGER);
content.getChildren().add(dangerTag);
return new SampleBlock("Filled", content);
}
private SampleBlock iconTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
var basicTag = new Tag("image", new FontIcon(Feather.IMAGE));
content.getChildren().add(basicTag);
var accentTag = new Tag("music", new FontIcon(Feather.MUSIC));
content.getChildren().add(accentTag);
var successTag = new Tag("video", new FontIcon(Feather.VIDEO));
content.getChildren().add(successTag);
return new SampleBlock("Icon", content);
}
private SampleBlock outlinedTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
var accentTag = new Tag("accent");
accentTag.getStyleClass().addAll(Styles.ACCENT, Styles.BUTTON_OUTLINED);
content.getChildren().add(accentTag);
var successTag = new Tag("success");
successTag.getStyleClass().addAll(Styles.SUCCESS, Styles.BUTTON_OUTLINED);
content.getChildren().add(successTag);
var dangerTag = new Tag("danger");
dangerTag.getStyleClass().addAll(Styles.DANGER, Styles.BUTTON_OUTLINED);
content.getChildren().add(dangerTag);
return new SampleBlock("Outlined", content);
}
private SampleBlock closeableTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
var basicTag = new Tag("basic", new FontIcon(Material2AL.CLOSE));
basicTag.setContentDisplay(ContentDisplay.RIGHT);
content.getChildren().add(basicTag);
var accentTag = new Tag("accent", new FontIcon(Material2AL.CANCEL));
accentTag.setContentDisplay(ContentDisplay.RIGHT);
accentTag.getStyleClass().add(Styles.ACCENT);
content.getChildren().add(accentTag);
var successTag = new Tag("success", new FontIcon(Material2AL.CANCEL));
successTag.setContentDisplay(ContentDisplay.RIGHT);
successTag.getStyleClass().add(Styles.SUCCESS);
content.getChildren().add(successTag);
var dangerTag = new Tag("danger", new FontIcon(Material2AL.CANCEL));
dangerTag.setContentDisplay(ContentDisplay.RIGHT);
dangerTag.getStyleClass().add(Styles.DANGER);
content.getChildren().add(dangerTag);
return new SampleBlock("Removable", content);
}
private SampleBlock customColorTagSample() {
var content = new FlowPane(BLOCK_HGAP, BLOCK_VGAP);
content.setPrefWidth(PREF_WIDTH);
new CSSFragment("""
.brand {
-color-button-fg: -color-fg-emphasis;
-color-button-bg-hover: -color-button-bg;
-color-button-bg-pressed: -color-button-bg;
}
.twitter {
-color-button-bg: rgb(85, 172, 238);
-color-button-border: rgb(85, 172, 238);
}
.youtube {
-color-button-bg: rgb(205, 32, 31);
-color-button-border: rgb(205, 32, 31);
}
.facebook {
-color-button-bg: rgb(59, 89, 153);
-color-button-border: rgb(59, 89, 153);
}
""").addTo(content);
var twitterTag = new Tag("Twitter", new FontIcon(Feather.TWITTER));
twitterTag.getStyleClass().addAll("brand", "twitter");
content.getChildren().add(twitterTag);
var youtubeTag = new Tag("YouTube", new FontIcon(Feather.YOUTUBE));
youtubeTag.getStyleClass().addAll("brand", "youtube");
content.getChildren().add(youtubeTag);
var facebookTag = new Tag("Facebook", new FontIcon(Feather.FACEBOOK));
facebookTag.getStyleClass().addAll("brand", "facebook");
content.getChildren().add(facebookTag);
return new SampleBlock("Custom Color", content);
}
}

@ -1,133 +0,0 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.showcase.widget;
import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import atlantafx.sampler.page.Page;
import java.util.function.Supplier;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
// JavaFX Skin API is very complex and almost undocumented. In many cases it's much simpler
// to create a small widget that just do the job than wasting hours to debug control behaviour.
// Consider this as a cookbook of those widgets.
public class WidgetCollectionPage extends BorderPane implements Page {
public static final String NAME = "Widgets";
@Override
public String getName() {
return NAME;
}
private final ListView<Example> toc = new ListView<>();
private final VBox widgetWrapper = new VBox(PAGE_HGAP);
private boolean isRendered = false;
public WidgetCollectionPage() {
super();
createView();
}
private void createView() {
widgetWrapper.getStyleClass().add("widget");
widgetWrapper.setAlignment(Pos.TOP_CENTER);
widgetWrapper.setFillWidth(false);
toc.setCellFactory(c -> new TocCell());
toc.getStyleClass().addAll("toc", Styles.DENSE, Tweaks.EDGE_TO_EDGE);
toc.getItems().setAll(Example.values());
toc.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
if (val == null) {
return;
}
widgetWrapper.getChildren().setAll(val.getSupplier().get());
});
// ~
setCenter(widgetWrapper);
setRight(toc);
BorderPane.setMargin(toc, new Insets(0, 0, 0, PAGE_HGAP));
getStyleClass().setAll("page", "widget-collection");
toc.getSelectionModel().selectFirst();
}
@Override
public Parent getView() {
return this;
}
@Override
public boolean canDisplaySourceCode() {
return false;
}
@Override
public boolean canChangeThemeSettings() {
return true;
}
@Override
public void reset() {
}
@Override
protected void layoutChildren() {
super.layoutChildren();
if (isRendered) {
return;
}
isRendered = true;
toc.getSelectionModel().selectFirst();
toc.requestFocus();
}
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings("ImmutableEnumChecker")
public enum Example {
CARD("Card", CardSample::new),
MESSAGE("Message", MessageSample::new),
STEPPER("Stepper", StepperSample::new),
TAG("Tag", TagSample::new);
private final String name;
private final Supplier<Pane> supplier;
Example(String name, Supplier<Pane> supplier) {
this.name = name;
this.supplier = supplier;
}
public String getName() {
return name;
}
public Supplier<Pane> getSupplier() {
return supplier;
}
}
private static class TocCell extends ListCell<Example> {
@Override
protected void updateItem(Example item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getName());
}
}
}
}

@ -4,18 +4,11 @@ package atlantafx.sampler.util;
import static atlantafx.base.theme.Styles.BUTTON_ICON; import static atlantafx.base.theme.Styles.BUTTON_ICON;
import atlantafx.sampler.event.BrowseEvent;
import atlantafx.sampler.event.DefaultEventBus;
import java.net.URI;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.input.KeyCombination;
import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@Deprecated
public final class Controls { public final class Controls {
public static Button iconButton(Ikon icon, boolean disable) { public static Button iconButton(Ikon icon, boolean disable) {
@ -31,48 +24,4 @@ public final class Controls {
button.getStyleClass().addAll(styleClasses); button.getStyleClass().addAll(styleClasses);
return button; return button;
} }
public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator) {
return menuItem(text, graphic, accelerator, false);
}
public static MenuItem menuItem(String text, Ikon graphic, KeyCombination accelerator, boolean disable) {
var item = new MenuItem(text);
if (graphic != null) {
item.setGraphic(new FontIcon(graphic));
}
if (accelerator != null) {
item.setAccelerator(accelerator);
}
item.setDisable(disable);
return item;
}
public static ToggleButton toggleButton(String text,
Ikon icon,
ToggleGroup group,
boolean selected,
String... styleClasses) {
var toggleButton = new ToggleButton(text);
if (icon != null) {
toggleButton.setGraphic(new FontIcon(icon));
}
if (group != null) {
toggleButton.setToggleGroup(group);
}
toggleButton.setSelected(selected);
toggleButton.getStyleClass().addAll(styleClasses);
return toggleButton;
}
public static Hyperlink hyperlink(String text, URI uri) {
var hyperlink = new Hyperlink(text);
if (uri != null) {
hyperlink.setOnAction(event -> DefaultEventBus.getInstance().publish(new BrowseEvent(uri)));
}
return hyperlink;
}
} }

@ -0,0 +1,39 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util;
import java.util.Objects;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
/**
* Auxiliary object wrapper to support lazy initialization.
* DO NOT override {@code hashCode()} / {@code equals()}, because each instance
* of this object must remain unique.
*/
public class Lazy<T> implements Supplier<T> {
protected final Supplier<T> supplier;
protected @Nullable T value = null;
public Lazy(Supplier<T> supplier) {
this.supplier = Objects.requireNonNull(supplier, "supplier");
}
@Override
public T get() {
if (value == null) {
value = supplier.get();
}
return value;
}
public boolean initialized() {
return value != null;
}
@Override
public String toString() {
return String.valueOf(value);
}
}

@ -9,6 +9,7 @@ module atlantafx.sampler {
requires javafx.swing; requires javafx.swing;
requires javafx.media; requires javafx.media;
requires javafx.web; requires javafx.web;
requires javafx.fxml;
requires org.kordamp.ikonli.core; requires org.kordamp.ikonli.core;
requires org.kordamp.ikonli.javafx; requires org.kordamp.ikonli.javafx;
@ -20,13 +21,13 @@ module atlantafx.sampler {
requires datafaker; requires datafaker;
exports atlantafx.sampler; exports atlantafx.sampler;
exports atlantafx.sampler.fake;
exports atlantafx.sampler.fake.domain; exports atlantafx.sampler.fake.domain;
exports atlantafx.sampler.event; exports atlantafx.sampler.event;
exports atlantafx.sampler.layout; exports atlantafx.sampler.layout;
exports atlantafx.sampler.page; exports atlantafx.sampler.page;
exports atlantafx.sampler.page.general; exports atlantafx.sampler.page.general;
exports atlantafx.sampler.page.components; exports atlantafx.sampler.page.components;
exports atlantafx.sampler.page.extras;
exports atlantafx.sampler.page.showcase; exports atlantafx.sampler.page.showcase;
exports atlantafx.sampler.theme; exports atlantafx.sampler.theme;
exports atlantafx.sampler.util; exports atlantafx.sampler.util;
@ -39,4 +40,5 @@ module atlantafx.sampler {
opens atlantafx.sampler.assets.styles; opens atlantafx.sampler.assets.styles;
opens atlantafx.sampler.images; opens atlantafx.sampler.images;
opens atlantafx.sampler.images.modena; opens atlantafx.sampler.images.modena;
opens atlantafx.sampler.page.general;
} }

Some files were not shown because too many files have changed in this diff Show More